q-fi-core 2.3.1
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 +109 -0
- package/LICENSE +1 -0
- package/README.md +186 -0
- package/dist/api/api.d.ts +78 -0
- package/dist/api/api.js +182 -0
- package/dist/api/cache.d.ts +3 -0
- package/dist/api/cache.js +12 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +18 -0
- package/dist/api/request.d.ts +23 -0
- package/dist/api/request.js +88 -0
- package/dist/converter/deezer.d.ts +3 -0
- package/dist/converter/deezer.js +45 -0
- package/dist/converter/index.d.ts +5 -0
- package/dist/converter/index.js +34 -0
- package/dist/converter/parse.d.ts +18 -0
- package/dist/converter/parse.js +176 -0
- package/dist/converter/spotify.d.ts +40 -0
- package/dist/converter/spotify.js +154 -0
- package/dist/converter/tidal.d.ts +146 -0
- package/dist/converter/tidal.js +199 -0
- package/dist/converter/youtube.d.ts +6 -0
- package/dist/converter/youtube.js +102 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +24 -0
- package/dist/lib/decrypt.d.ts +9 -0
- package/dist/lib/decrypt.js +63 -0
- package/dist/lib/fast-lru.d.ts +63 -0
- package/dist/lib/fast-lru.js +121 -0
- package/dist/lib/get-url.d.ts +16 -0
- package/dist/lib/get-url.js +154 -0
- package/dist/lib/http.d.ts +66 -0
- package/dist/lib/http.js +235 -0
- package/dist/lib/metaflac-js.d.ts +124 -0
- package/dist/lib/metaflac-js.js +343 -0
- package/dist/lib/request.d.ts +18 -0
- package/dist/lib/request.js +86 -0
- package/dist/metadata-writer/abumCover.d.ts +10 -0
- package/dist/metadata-writer/abumCover.js +37 -0
- package/dist/metadata-writer/flacmetata.d.ts +3 -0
- package/dist/metadata-writer/flacmetata.js +80 -0
- package/dist/metadata-writer/getTrackLyrics.d.ts +2 -0
- package/dist/metadata-writer/getTrackLyrics.js +26 -0
- package/dist/metadata-writer/id3.d.ts +3 -0
- package/dist/metadata-writer/id3.js +119 -0
- package/dist/metadata-writer/index.d.ts +9 -0
- package/dist/metadata-writer/index.js +44 -0
- package/dist/metadata-writer/musixmatchLyrics.d.ts +1 -0
- package/dist/metadata-writer/musixmatchLyrics.js +39 -0
- package/dist/metadata-writer/useragents.d.ts +1 -0
- package/dist/metadata-writer/useragents.js +86 -0
- package/dist/types/album.d.ts +123 -0
- package/dist/types/album.js +2 -0
- package/dist/types/artist.d.ts +36 -0
- package/dist/types/artist.js +2 -0
- package/dist/types/channel.d.ts +33 -0
- package/dist/types/channel.js +2 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.js +26 -0
- package/dist/types/playlist-channel.d.ts +81 -0
- package/dist/types/playlist-channel.js +2 -0
- package/dist/types/playlist.d.ts +49 -0
- package/dist/types/playlist.js +2 -0
- package/dist/types/profile.d.ts +29 -0
- package/dist/types/profile.js +2 -0
- package/dist/types/radio.d.ts +7 -0
- package/dist/types/radio.js +2 -0
- package/dist/types/search.d.ts +79 -0
- package/dist/types/search.js +2 -0
- package/dist/types/show.d.ts +61 -0
- package/dist/types/show.js +2 -0
- package/dist/types/tracks.d.ts +144 -0
- package/dist/types/tracks.js +2 -0
- package/dist/types/user.d.ts +16 -0
- package/dist/types/user.js +2 -0
- package/package.json +69 -0
- package/types/index.d.ts +1 -0
- package/types/index.js +3 -0
- package/types/package.json +4 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
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
|
+
class FastLRU {
|
|
9
|
+
constructor({ maxSize = Infinity, ttl = 0 }) {
|
|
10
|
+
// Default options
|
|
11
|
+
this._max = maxSize;
|
|
12
|
+
this._ttl = ttl;
|
|
13
|
+
this._cache = new Map();
|
|
14
|
+
// Metadata for entries
|
|
15
|
+
this._meta = {};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Add new entry
|
|
19
|
+
*/
|
|
20
|
+
set(key, value, ttl = this._ttl) {
|
|
21
|
+
// Execution time
|
|
22
|
+
const time = Date.now();
|
|
23
|
+
// Remvove least recently used elements if exceeds max bytes
|
|
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
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Override if key already set
|
|
39
|
+
this._cache.set(key, value);
|
|
40
|
+
this._meta[key] = {
|
|
41
|
+
key,
|
|
42
|
+
hits: 0,
|
|
43
|
+
expire: time + ttl,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get entry
|
|
48
|
+
*/
|
|
49
|
+
get(key) {
|
|
50
|
+
if (this._cache.has(key)) {
|
|
51
|
+
const item = this._cache.get(key);
|
|
52
|
+
if (this._ttl > 0) {
|
|
53
|
+
const time = Date.now();
|
|
54
|
+
if (this._meta[key].expire < time) {
|
|
55
|
+
this.delete(key);
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
this._meta[key].hits++;
|
|
60
|
+
return item;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get without hitting hits
|
|
65
|
+
*/
|
|
66
|
+
peek(key) {
|
|
67
|
+
return this._cache.get(key);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Remove entry
|
|
71
|
+
*/
|
|
72
|
+
delete(key) {
|
|
73
|
+
delete this._meta[key];
|
|
74
|
+
this._cache.delete(key);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Remove all entries
|
|
78
|
+
*/
|
|
79
|
+
clear() {
|
|
80
|
+
this._cache.clear();
|
|
81
|
+
this._meta = {};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Check has entry
|
|
85
|
+
*/
|
|
86
|
+
has(key) {
|
|
87
|
+
return this._cache.has(key);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Get all kies
|
|
91
|
+
* @returns {Iterator} Iterator on all kies
|
|
92
|
+
*/
|
|
93
|
+
keys() {
|
|
94
|
+
return this._cache.keys();
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Iterate over values
|
|
98
|
+
*/
|
|
99
|
+
values() {
|
|
100
|
+
return this._cache.values();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Iterate over entries
|
|
104
|
+
*/
|
|
105
|
+
entries() {
|
|
106
|
+
return this._cache.entries();
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* For each
|
|
110
|
+
*/
|
|
111
|
+
forEach(cb) {
|
|
112
|
+
return this._cache.forEach(cb);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Entries total size
|
|
116
|
+
*/
|
|
117
|
+
get size() {
|
|
118
|
+
return this._cache.size;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
exports.default = FastLRU;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { trackType } from '../types';
|
|
2
|
+
export declare class WrongLicense extends Error {
|
|
3
|
+
constructor(format: string);
|
|
4
|
+
}
|
|
5
|
+
export declare class GeoBlocked extends Error {
|
|
6
|
+
constructor(country: string);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* @param track Track info json returned from `getTrackInfo`
|
|
10
|
+
* @param quality 1 = 128kbps, 3 = 320kbps and 9 = flac (around 1411kbps)
|
|
11
|
+
*/
|
|
12
|
+
export declare const getTrackDownloadUrl: (track: trackType, quality: number) => Promise<{
|
|
13
|
+
trackUrl: string;
|
|
14
|
+
isEncrypted: boolean;
|
|
15
|
+
fileSize: number;
|
|
16
|
+
} | null>;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getTrackDownloadUrl = exports.GeoBlocked = exports.WrongLicense = void 0;
|
|
7
|
+
const decrypt_1 = require("../lib/decrypt");
|
|
8
|
+
const http_1 = require("../lib/http");
|
|
9
|
+
const request_1 = __importDefault(require("../lib/request"));
|
|
10
|
+
class WrongLicense extends Error {
|
|
11
|
+
constructor(format) {
|
|
12
|
+
super();
|
|
13
|
+
this.name = 'WrongLicense';
|
|
14
|
+
this.message = `Your account can't stream ${format} tracks`;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.WrongLicense = WrongLicense;
|
|
18
|
+
class GeoBlocked extends Error {
|
|
19
|
+
constructor(country) {
|
|
20
|
+
super();
|
|
21
|
+
this.name = 'GeoBlocked';
|
|
22
|
+
this.message = `This track is not available in your country (${country})`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.GeoBlocked = GeoBlocked;
|
|
26
|
+
let user_data = null;
|
|
27
|
+
const getTrackFileSize = (track, quality) => {
|
|
28
|
+
switch (quality) {
|
|
29
|
+
case 9:
|
|
30
|
+
return Number(track.FILESIZE_FLAC);
|
|
31
|
+
case 3:
|
|
32
|
+
return Number(track.FILESIZE_MP3_320);
|
|
33
|
+
case 1:
|
|
34
|
+
return Number(track.FILESIZE_MP3_128);
|
|
35
|
+
default:
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const dzAuthenticate = async () => {
|
|
40
|
+
const { data } = await request_1.default.get('https://www.deezer.com/ajax/gw-light.php', {
|
|
41
|
+
params: {
|
|
42
|
+
method: 'deezer.getUserData',
|
|
43
|
+
api_version: '1.0',
|
|
44
|
+
api_token: 'null',
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
user_data = {
|
|
48
|
+
license_token: data.results.USER.OPTIONS.license_token,
|
|
49
|
+
can_stream_lossless: data.results.USER.OPTIONS.web_lossless || data.results.USER.OPTIONS.mobile_loseless,
|
|
50
|
+
can_stream_hq: data.results.USER.OPTIONS.web_hq || data.results.USER.OPTIONS.mobile_hq,
|
|
51
|
+
country: data.results.COUNTRY,
|
|
52
|
+
};
|
|
53
|
+
return user_data;
|
|
54
|
+
};
|
|
55
|
+
const getTrackUrlFromServer = async (track_token, format) => {
|
|
56
|
+
const user = user_data ? user_data : await dzAuthenticate();
|
|
57
|
+
if ((format === 'FLAC' && !user.can_stream_lossless) || (format === 'MP3_320' && !user.can_stream_hq)) {
|
|
58
|
+
throw new WrongLicense(format);
|
|
59
|
+
}
|
|
60
|
+
const { data } = await request_1.default.post('https://media.deezer.com/v1/get_url', {
|
|
61
|
+
license_token: user.license_token,
|
|
62
|
+
media: [
|
|
63
|
+
{
|
|
64
|
+
type: 'FULL',
|
|
65
|
+
formats: [{ format, cipher: 'BF_CBC_STRIPE' }],
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
track_tokens: [track_token],
|
|
69
|
+
});
|
|
70
|
+
if (data.data.length > 0) {
|
|
71
|
+
if (data.data[0].errors) {
|
|
72
|
+
if (data.data[0].errors[0].code === 2002) {
|
|
73
|
+
throw new GeoBlocked(user.country);
|
|
74
|
+
}
|
|
75
|
+
throw new Error(Object.entries(data.data[0].errors[0]).join(', '));
|
|
76
|
+
}
|
|
77
|
+
return data.data[0].media.length > 0 ? data.data[0].media[0].sources[0].url : null;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* @param track Track info json returned from `getTrackInfo`
|
|
83
|
+
* @param quality 1 = 128kbps, 3 = 320kbps and 9 = flac (around 1411kbps)
|
|
84
|
+
*/
|
|
85
|
+
const getTrackDownloadUrl = async (track, quality) => {
|
|
86
|
+
let wrongLicense = null;
|
|
87
|
+
let geoBlocked = null;
|
|
88
|
+
let formatName;
|
|
89
|
+
switch (quality) {
|
|
90
|
+
case 9:
|
|
91
|
+
formatName = 'FLAC';
|
|
92
|
+
break;
|
|
93
|
+
case 3:
|
|
94
|
+
formatName = 'MP3_320';
|
|
95
|
+
break;
|
|
96
|
+
case 1:
|
|
97
|
+
formatName = 'MP3_128';
|
|
98
|
+
break;
|
|
99
|
+
default:
|
|
100
|
+
throw new Error(`Unknown quality ${quality}`);
|
|
101
|
+
}
|
|
102
|
+
// Get URL with the official API
|
|
103
|
+
try {
|
|
104
|
+
const url = await getTrackUrlFromServer(track.TRACK_TOKEN, formatName);
|
|
105
|
+
if (url) {
|
|
106
|
+
return {
|
|
107
|
+
trackUrl: url,
|
|
108
|
+
isEncrypted: url.includes('/mobile/') || url.includes('/media/'),
|
|
109
|
+
fileSize: getTrackFileSize(track, quality),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
if (err instanceof WrongLicense) {
|
|
115
|
+
wrongLicense = err;
|
|
116
|
+
}
|
|
117
|
+
else if (err instanceof GeoBlocked) {
|
|
118
|
+
geoBlocked = err;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Fallback to the old method
|
|
125
|
+
if (track.MD5_ORIGIN) {
|
|
126
|
+
const filename = (0, decrypt_1.getSongFileName)(track, quality); // encrypted file name
|
|
127
|
+
const url = `https://e-cdns-proxy-${track.MD5_ORIGIN[0]}.dzcdn.net/mobile/1/${filename}`;
|
|
128
|
+
const fileSize = await testUrl(url);
|
|
129
|
+
if (fileSize > 0) {
|
|
130
|
+
return {
|
|
131
|
+
trackUrl: url,
|
|
132
|
+
isEncrypted: url.includes('/mobile/') || url.includes('/media/'),
|
|
133
|
+
fileSize: fileSize,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (wrongLicense) {
|
|
138
|
+
throw wrongLicense;
|
|
139
|
+
}
|
|
140
|
+
if (geoBlocked) {
|
|
141
|
+
throw geoBlocked;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
};
|
|
145
|
+
exports.getTrackDownloadUrl = getTrackDownloadUrl;
|
|
146
|
+
const testUrl = async (url) => {
|
|
147
|
+
try {
|
|
148
|
+
const { headers } = await (0, http_1.headRequest)(url);
|
|
149
|
+
return Number(headers['content-length']);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
import { IncomingHttpHeaders } from 'http';
|
|
4
|
+
type HttpMethod = 'GET' | 'POST' | 'HEAD';
|
|
5
|
+
type ResponseType = 'buffer' | 'json' | 'text';
|
|
6
|
+
type QueryValue = string | number | boolean | null | undefined;
|
|
7
|
+
type QueryInput = QueryValue | QueryValue[];
|
|
8
|
+
export type HttpHeaders = Record<string, string>;
|
|
9
|
+
export type HttpQuery = Record<string, QueryInput>;
|
|
10
|
+
export interface HttpRequestConfig {
|
|
11
|
+
headers?: HttpHeaders;
|
|
12
|
+
params?: HttpQuery;
|
|
13
|
+
timeout?: number;
|
|
14
|
+
responseType?: ResponseType;
|
|
15
|
+
}
|
|
16
|
+
interface RequestDescriptor {
|
|
17
|
+
data?: unknown;
|
|
18
|
+
headers?: HttpHeaders;
|
|
19
|
+
method: HttpMethod;
|
|
20
|
+
params?: HttpQuery;
|
|
21
|
+
responseType: ResponseType;
|
|
22
|
+
timeout: number;
|
|
23
|
+
url: string;
|
|
24
|
+
}
|
|
25
|
+
export interface HttpResponse<T> {
|
|
26
|
+
config: RequestDescriptor;
|
|
27
|
+
data: T;
|
|
28
|
+
headers: IncomingHttpHeaders;
|
|
29
|
+
request: {
|
|
30
|
+
res: {
|
|
31
|
+
responseUrl: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
status: number;
|
|
35
|
+
}
|
|
36
|
+
export declare class HttpStatusError extends Error {
|
|
37
|
+
body: Buffer;
|
|
38
|
+
headers: IncomingHttpHeaders;
|
|
39
|
+
statusCode: number;
|
|
40
|
+
constructor(statusCode: number, headers: IncomingHttpHeaders, body: Buffer);
|
|
41
|
+
}
|
|
42
|
+
export declare class HttpClient {
|
|
43
|
+
defaults: {
|
|
44
|
+
baseURL?: string;
|
|
45
|
+
headers: HttpHeaders;
|
|
46
|
+
maxRedirects: number;
|
|
47
|
+
params: HttpQuery;
|
|
48
|
+
timeout: number;
|
|
49
|
+
};
|
|
50
|
+
constructor(defaults?: {
|
|
51
|
+
baseURL?: string;
|
|
52
|
+
headers?: HttpHeaders;
|
|
53
|
+
maxRedirects?: number;
|
|
54
|
+
params?: HttpQuery;
|
|
55
|
+
timeout?: number;
|
|
56
|
+
});
|
|
57
|
+
get<T = unknown>(url: string, config?: HttpRequestConfig): Promise<HttpResponse<T>>;
|
|
58
|
+
post<T = unknown>(url: string, body?: unknown, config?: HttpRequestConfig): Promise<HttpResponse<T>>;
|
|
59
|
+
head(url: string, config?: Omit<HttpRequestConfig, 'responseType'>): Promise<HttpResponse<Buffer>>;
|
|
60
|
+
private request;
|
|
61
|
+
}
|
|
62
|
+
export declare const getBuffer: (url: string, config?: HttpRequestConfig) => Promise<Buffer>;
|
|
63
|
+
export declare const getJson: <T = unknown>(url: string, config?: HttpRequestConfig) => Promise<T>;
|
|
64
|
+
export declare const getText: (url: string, config?: HttpRequestConfig) => Promise<string>;
|
|
65
|
+
export declare const headRequest: (url: string, config?: Omit<HttpRequestConfig, 'responseType'>) => Promise<HttpResponse<Buffer>>;
|
|
66
|
+
export {};
|
package/dist/lib/http.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.headRequest = exports.getText = exports.getJson = exports.getBuffer = exports.HttpClient = exports.HttpStatusError = void 0;
|
|
4
|
+
const http_1 = require("http");
|
|
5
|
+
const https_1 = require("https");
|
|
6
|
+
const zlib_1 = require("zlib");
|
|
7
|
+
const httpAgent = new http_1.Agent({ keepAlive: true });
|
|
8
|
+
const httpsAgent = new https_1.Agent({ keepAlive: true });
|
|
9
|
+
class HttpStatusError extends Error {
|
|
10
|
+
constructor(statusCode, headers, body) {
|
|
11
|
+
super(`Request failed with status code ${statusCode}`);
|
|
12
|
+
this.name = 'HttpStatusError';
|
|
13
|
+
this.statusCode = statusCode;
|
|
14
|
+
this.headers = headers;
|
|
15
|
+
this.body = body;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.HttpStatusError = HttpStatusError;
|
|
19
|
+
class HttpClient {
|
|
20
|
+
constructor(defaults = {}) {
|
|
21
|
+
var _a, _b;
|
|
22
|
+
this.defaults = {
|
|
23
|
+
baseURL: defaults.baseURL,
|
|
24
|
+
headers: defaults.headers ? normalizeHeaders(defaults.headers) : {},
|
|
25
|
+
maxRedirects: (_a = defaults.maxRedirects) !== null && _a !== void 0 ? _a : 5,
|
|
26
|
+
params: defaults.params ? { ...defaults.params } : {},
|
|
27
|
+
timeout: (_b = defaults.timeout) !== null && _b !== void 0 ? _b : 15000,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
get(url, config = {}) {
|
|
31
|
+
return this.request('GET', url, undefined, config);
|
|
32
|
+
}
|
|
33
|
+
post(url, body, config = {}) {
|
|
34
|
+
return this.request('POST', url, body, config);
|
|
35
|
+
}
|
|
36
|
+
head(url, config = {}) {
|
|
37
|
+
return this.request('HEAD', url, undefined, { ...config, responseType: 'buffer' });
|
|
38
|
+
}
|
|
39
|
+
async request(method, url, body, config = {}) {
|
|
40
|
+
var _a, _b;
|
|
41
|
+
const responseType = (_a = config.responseType) !== null && _a !== void 0 ? _a : 'json';
|
|
42
|
+
const timeout = (_b = config.timeout) !== null && _b !== void 0 ? _b : this.defaults.timeout;
|
|
43
|
+
const headers = normalizeHeaders({ ...this.defaults.headers, ...config.headers });
|
|
44
|
+
const params = { ...this.defaults.params, ...config.params };
|
|
45
|
+
const rawResponse = await requestRaw({
|
|
46
|
+
baseURL: this.defaults.baseURL,
|
|
47
|
+
body,
|
|
48
|
+
headers,
|
|
49
|
+
maxRedirects: this.defaults.maxRedirects,
|
|
50
|
+
method,
|
|
51
|
+
params,
|
|
52
|
+
timeout,
|
|
53
|
+
url,
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
config: {
|
|
57
|
+
data: body,
|
|
58
|
+
headers,
|
|
59
|
+
method,
|
|
60
|
+
params,
|
|
61
|
+
responseType,
|
|
62
|
+
timeout,
|
|
63
|
+
url,
|
|
64
|
+
},
|
|
65
|
+
data: parseResponseBody(rawResponse, responseType),
|
|
66
|
+
headers: rawResponse.headers,
|
|
67
|
+
request: {
|
|
68
|
+
res: {
|
|
69
|
+
responseUrl: rawResponse.url,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
status: rawResponse.status,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.HttpClient = HttpClient;
|
|
77
|
+
const requestRaw = async (config, redirectCount = 0) => {
|
|
78
|
+
const requestUrl = buildUrl(config.url, config.baseURL, config.params);
|
|
79
|
+
const headers = normalizeHeaders(config.headers);
|
|
80
|
+
const serializedBody = serializeBody(config.body, headers);
|
|
81
|
+
if (serializedBody && !headers['content-length']) {
|
|
82
|
+
headers['content-length'] = String(serializedBody.length);
|
|
83
|
+
}
|
|
84
|
+
return await new Promise((resolve, reject) => {
|
|
85
|
+
var _a;
|
|
86
|
+
const isHttps = requestUrl.protocol === 'https:';
|
|
87
|
+
const requestFn = isHttps ? https_1.request : http_1.request;
|
|
88
|
+
const req = requestFn({
|
|
89
|
+
agent: isHttps ? httpsAgent : httpAgent,
|
|
90
|
+
headers,
|
|
91
|
+
hostname: requestUrl.hostname,
|
|
92
|
+
method: config.method,
|
|
93
|
+
path: requestUrl.pathname + requestUrl.search,
|
|
94
|
+
port: requestUrl.port,
|
|
95
|
+
protocol: requestUrl.protocol,
|
|
96
|
+
}, (response) => {
|
|
97
|
+
var _a, _b;
|
|
98
|
+
const status = (_a = response.statusCode) !== null && _a !== void 0 ? _a : 0;
|
|
99
|
+
const locationHeader = response.headers.location;
|
|
100
|
+
const location = Array.isArray(locationHeader) ? locationHeader[0] : locationHeader;
|
|
101
|
+
if (location && isRedirectStatus(status) && redirectCount < ((_b = config.maxRedirects) !== null && _b !== void 0 ? _b : 5)) {
|
|
102
|
+
response.resume();
|
|
103
|
+
const redirectedMethod = status === 303 || ((status === 301 || status === 302) && config.method === 'POST') ? 'GET' : config.method;
|
|
104
|
+
const redirectedHeaders = { ...headers };
|
|
105
|
+
if (redirectedMethod === 'GET' || redirectedMethod === 'HEAD') {
|
|
106
|
+
delete redirectedHeaders['content-length'];
|
|
107
|
+
delete redirectedHeaders['content-type'];
|
|
108
|
+
}
|
|
109
|
+
requestRaw({
|
|
110
|
+
...config,
|
|
111
|
+
baseURL: undefined,
|
|
112
|
+
body: redirectedMethod === 'POST' ? config.body : undefined,
|
|
113
|
+
headers: redirectedHeaders,
|
|
114
|
+
method: redirectedMethod,
|
|
115
|
+
url: new URL(location, requestUrl).toString(),
|
|
116
|
+
}, redirectCount + 1)
|
|
117
|
+
.then(resolve)
|
|
118
|
+
.catch(reject);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const chunks = [];
|
|
122
|
+
response.on('data', (chunk) => {
|
|
123
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
124
|
+
});
|
|
125
|
+
response.on('end', () => {
|
|
126
|
+
try {
|
|
127
|
+
const body = decodeResponseBuffer(Buffer.concat(chunks), response.headers['content-encoding']);
|
|
128
|
+
if (status < 200 || status >= 300) {
|
|
129
|
+
reject(new HttpStatusError(status, response.headers, body));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
resolve({
|
|
133
|
+
body,
|
|
134
|
+
headers: response.headers,
|
|
135
|
+
status,
|
|
136
|
+
url: requestUrl.toString(),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
reject(err);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
req.on('error', reject);
|
|
145
|
+
req.setTimeout((_a = config.timeout) !== null && _a !== void 0 ? _a : 15000, () => {
|
|
146
|
+
var _a;
|
|
147
|
+
req.destroy(new Error(`Request timed out after ${(_a = config.timeout) !== null && _a !== void 0 ? _a : 15000}ms`));
|
|
148
|
+
});
|
|
149
|
+
if (serializedBody) {
|
|
150
|
+
req.write(serializedBody);
|
|
151
|
+
}
|
|
152
|
+
req.end();
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
const buildUrl = (url, baseURL, params) => {
|
|
156
|
+
const parsedUrl = new URL(resolveUrl(url, baseURL));
|
|
157
|
+
if (params) {
|
|
158
|
+
for (const [key, input] of Object.entries(params)) {
|
|
159
|
+
if (Array.isArray(input)) {
|
|
160
|
+
for (const value of input) {
|
|
161
|
+
if (value !== undefined && value !== null) {
|
|
162
|
+
parsedUrl.searchParams.append(key, String(value));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else if (input !== undefined && input !== null) {
|
|
167
|
+
parsedUrl.searchParams.set(key, String(input));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return parsedUrl;
|
|
172
|
+
};
|
|
173
|
+
const resolveUrl = (url, baseURL) => {
|
|
174
|
+
if (!baseURL || isAbsoluteUrl(url)) {
|
|
175
|
+
return url;
|
|
176
|
+
}
|
|
177
|
+
return `${baseURL.replace(/\/+$/, '')}/${url.replace(/^\/+/, '')}`;
|
|
178
|
+
};
|
|
179
|
+
const normalizeHeaders = (headers = {}) => {
|
|
180
|
+
const normalizedHeaders = {};
|
|
181
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
182
|
+
normalizedHeaders[key.toLowerCase()] = value;
|
|
183
|
+
}
|
|
184
|
+
return normalizedHeaders;
|
|
185
|
+
};
|
|
186
|
+
const serializeBody = (body, headers) => {
|
|
187
|
+
if (body === undefined) {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
if (Buffer.isBuffer(body)) {
|
|
191
|
+
return body;
|
|
192
|
+
}
|
|
193
|
+
if (typeof body === 'string') {
|
|
194
|
+
return Buffer.from(body);
|
|
195
|
+
}
|
|
196
|
+
if (!headers['content-type']) {
|
|
197
|
+
headers['content-type'] = 'application/json; charset=UTF-8';
|
|
198
|
+
}
|
|
199
|
+
return Buffer.from(JSON.stringify(body));
|
|
200
|
+
};
|
|
201
|
+
const decodeResponseBuffer = (buffer, contentEncoding) => {
|
|
202
|
+
var _a, _b;
|
|
203
|
+
const encoding = (_b = (_a = (Array.isArray(contentEncoding) ? contentEncoding[0] : contentEncoding)) === null || _a === void 0 ? void 0 : _a.split(',')[0]) === null || _b === void 0 ? void 0 : _b.trim();
|
|
204
|
+
switch (encoding) {
|
|
205
|
+
case 'br':
|
|
206
|
+
return (0, zlib_1.brotliDecompressSync)(buffer);
|
|
207
|
+
case 'deflate':
|
|
208
|
+
return (0, zlib_1.inflateSync)(buffer);
|
|
209
|
+
case 'gzip':
|
|
210
|
+
return (0, zlib_1.gunzipSync)(buffer);
|
|
211
|
+
default:
|
|
212
|
+
return buffer;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
const parseResponseBody = (response, responseType) => {
|
|
216
|
+
if (responseType === 'buffer') {
|
|
217
|
+
return response.body;
|
|
218
|
+
}
|
|
219
|
+
const text = response.body.toString('utf-8');
|
|
220
|
+
if (responseType === 'text') {
|
|
221
|
+
return text;
|
|
222
|
+
}
|
|
223
|
+
return JSON.parse(text);
|
|
224
|
+
};
|
|
225
|
+
const isRedirectStatus = (status) => [301, 302, 303, 307, 308].includes(status);
|
|
226
|
+
const isAbsoluteUrl = (url) => /^[a-z][a-z\d+\-.]*:\/\//i.test(url);
|
|
227
|
+
const defaultClient = new HttpClient();
|
|
228
|
+
const getBuffer = async (url, config = {}) => (await defaultClient.get(url, { ...config, responseType: 'buffer' })).data;
|
|
229
|
+
exports.getBuffer = getBuffer;
|
|
230
|
+
const getJson = async (url, config = {}) => (await defaultClient.get(url, { ...config, responseType: 'json' })).data;
|
|
231
|
+
exports.getJson = getJson;
|
|
232
|
+
const getText = async (url, config = {}) => (await defaultClient.get(url, { ...config, responseType: 'text' })).data;
|
|
233
|
+
exports.getText = getText;
|
|
234
|
+
const headRequest = async (url, config = {}) => await defaultClient.head(url, config);
|
|
235
|
+
exports.headRequest = headRequest;
|