@rrrublev/wb-private-api 0.8.5

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.
@@ -0,0 +1,527 @@
1
+ /* eslint-disable camelcase */
2
+ const format = require("string-format");
3
+ const Constants = require("./Constants");
4
+ const WBProduct = require("./WBProduct");
5
+ const Utils = require("./Utils");
6
+ const WBCatalog = require("./WBCatalog");
7
+ const SessionBuilder = require("./SessionBuilder");
8
+
9
+ async function mapWithConcurrency(items, limit, mapper) {
10
+ const results = new Array(items.length);
11
+ let nextIndex = 0;
12
+
13
+ async function worker() {
14
+ while (nextIndex < items.length) {
15
+ const i = nextIndex++;
16
+ results[i] = await mapper(items[i], i);
17
+ }
18
+ }
19
+
20
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
21
+ return results;
22
+ }
23
+
24
+ class WBPrivateAPI {
25
+ /* Creating a new instance of the class WBPrivateAPI. */
26
+ constructor({ destination, wbaasToken } = {}) {
27
+ this.session = SessionBuilder.create();
28
+ this.destination = destination;
29
+ this.dest = destination?.ids?.at(-1) ?? null;
30
+ const token = wbaasToken || SessionBuilder.readToken();
31
+ if (token) {
32
+ SessionBuilder.setAntibotToken(this.session, token);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Устанавливает токен x_wbaas_token для доступа к внутренним API WB.
38
+ * Получить токен можно через консоль браузера на wildberries.ru:
39
+ * @see scripts/get-wb-token.js
40
+ * @param {string} token — значение cookie x_wbaas_token
41
+ */
42
+ setToken(token) {
43
+ SessionBuilder.setAntibotToken(this.session, token);
44
+ }
45
+
46
+ /**
47
+ * It searches for products by keyword.
48
+ * @param {string} keyword - The keyword to search for
49
+ * @param {number} pageCount - Number of pages to retrieve
50
+ * @returns {WBCatalog} WBCatalog object with plain product objects inside
51
+ */
52
+ async search(keyword, pageCount = 0, retries = 0, filters = []) {
53
+ const products = [];
54
+
55
+ const totalProducts = await this.searchTotalProducts(keyword);
56
+ if (totalProducts === 0) {
57
+ return new WBCatalog({
58
+ keyword,
59
+ catalog_type: null,
60
+ catalog_value: null,
61
+ pages: 0,
62
+ products: [],
63
+ totalProducts: 0,
64
+ });
65
+ }
66
+
67
+ const { catalog_type, catalog_value } = await this.getQueryMetadata(keyword, 0, false, 1, retries);
68
+ const catalogConfig = { keyword, catalog_type, catalog_value };
69
+
70
+ let totalPages = this.getPageCount(totalProducts);
71
+
72
+ if (pageCount > 0 && pageCount < totalPages) {
73
+ totalPages = pageCount;
74
+ }
75
+
76
+ const threads = Array.from({ length: totalPages }, (_, i) => i + 1);
77
+ const parsedPages = await mapWithConcurrency(threads, 5, (thr) => this.getCatalogPage(catalogConfig, thr, retries, filters));
78
+
79
+ const productOptions = { session: this.session, destination: this.destination };
80
+ for (const page of parsedPages) {
81
+ if (Array.isArray(page)) {
82
+ products.push(...page.map((v) => new WBProduct(v, productOptions)));
83
+ }
84
+ }
85
+
86
+ Object.assign(catalogConfig, {
87
+ pages: totalPages,
88
+ products,
89
+ totalProducts,
90
+ });
91
+
92
+ return new WBCatalog(catalogConfig);
93
+ }
94
+
95
+ /**
96
+ * It takes a keyword and returns an array of three elements,
97
+ * shardKey, preset and preset value
98
+ * @param {string} keyword - The keyword you want to search for.
99
+ * @returns {array} - An array of shardKey, preset and preset value
100
+ */
101
+ async getQueryMetadata(keyword, limit = 0, _withProducts = false, page = 1, retries = 0, suppressSpellcheck = true) {
102
+ const params = {
103
+ appType: Constants.APPTYPES.DESKTOP,
104
+ curr: Constants.CURRENCIES.RUB,
105
+ dest: this.dest,
106
+ query: keyword,
107
+ resultset: "catalog",
108
+ sort: "popular",
109
+ spp: "30",
110
+ suppressSpellcheck,
111
+ };
112
+ if (page !== 1) {
113
+ params.page = page;
114
+ }
115
+ if (limit !== 100) {
116
+ params.limit = limit;
117
+ }
118
+
119
+ const res = await this.session.get(Constants.URLS.SEARCH.EXACTMATCH, {
120
+ params,
121
+ headers: {
122
+ "x-queryid": Utils.Search.getQueryIdForSearch(),
123
+ },
124
+ retryOptions: { retries },
125
+ });
126
+
127
+ if ("catalog_type" in (res.data?.metadata ?? {}) && "catalog_value" in (res.data?.metadata ?? {})) {
128
+ return { ...res.data.metadata, products: res.data.data?.products ?? res.data.products };
129
+ }
130
+
131
+ if ("shardKey" in (res.data ?? {}) && "query" in (res.data ?? {})) {
132
+ return {
133
+ catalog_type: res.data.shardKey,
134
+ catalog_value: res.data.query,
135
+ products: [],
136
+ };
137
+ }
138
+
139
+ return { catalog_type: null, catalog_value: null, products: [] };
140
+ }
141
+
142
+ /**
143
+ * It returns the total number of products for a given keyword
144
+ * @param {string} keyword - the search query
145
+ * @returns Total number of products
146
+ */
147
+ async searchTotalProducts(keyword) {
148
+ const res = await this.session.get(Constants.URLS.SEARCH.EXACTMATCH, {
149
+ params: {
150
+ appType: Constants.APPTYPES.DESKTOP,
151
+ curr: Constants.CURRENCIES.RUB,
152
+ locale: Constants.LOCALES.RU,
153
+ lang: Constants.LOCALES.RU,
154
+ dest: this.dest,
155
+ query: keyword,
156
+ resultset: "catalog",
157
+ limit: 0,
158
+ },
159
+ headers: {
160
+ "x-queryid": Utils.Search.getQueryIdForSearch(),
161
+ },
162
+ });
163
+
164
+ return res.data.total || 0;
165
+ }
166
+
167
+ /**
168
+ * It returns the total number of products by supplier
169
+ * @param {number} supplierId - the search query
170
+ * @returns {number} Total number of products
171
+ */
172
+ async getSupplierProductCount(supplierId) {
173
+ const res = await this.session.get(Constants.URLS.SUPPLIER.CATALOG, {
174
+ params: {
175
+ appType: Constants.APPTYPES.DESKTOP,
176
+ curr: Constants.CURRENCIES.RUB,
177
+ dest: this.dest,
178
+ supplier: supplierId,
179
+ limit: 0,
180
+ },
181
+ });
182
+
183
+ return res.data.total || 0;
184
+ }
185
+
186
+ /**
187
+ * @returns {number} Total number of products for a brand
188
+ */
189
+ async getBrandProductCount(brandId) {
190
+ const res = await this.session.get(Constants.URLS.BRAND.CATALOG, {
191
+ params: {
192
+ appType: Constants.APPTYPES.DESKTOP,
193
+ curr: Constants.CURRENCIES.RUB,
194
+ dest: this.dest,
195
+ brand: brandId,
196
+ limit: 0,
197
+ },
198
+ });
199
+ return res.data.total || 0;
200
+ }
201
+
202
+ /**
203
+ * @returns {Promise<object>} Raw API response for brand catalog page
204
+ */
205
+ async getBrandCatalog(brandId, page = 1) {
206
+ const res = await this.session.get(Constants.URLS.BRAND.CATALOG, {
207
+ params: {
208
+ appType: Constants.APPTYPES.DESKTOP,
209
+ curr: Constants.CURRENCIES.RUB,
210
+ dest: this.dest,
211
+ lang: Constants.LOCALES.RU,
212
+ page,
213
+ sort: "popular",
214
+ spp: "30",
215
+ brand: brandId,
216
+ },
217
+ });
218
+ return res.data || {};
219
+ }
220
+
221
+ /**
222
+ * @returns {array} Products from a brand catalog page
223
+ */
224
+ async getBrandCatalogPage(brandId, page = 1, retries = 0) {
225
+ const res = await this.session.get(Constants.URLS.BRAND.CATALOG, {
226
+ params: {
227
+ appType: Constants.APPTYPES.DESKTOP,
228
+ curr: Constants.CURRENCIES.RUB,
229
+ dest: this.dest,
230
+ lang: Constants.LOCALES.RU,
231
+ page,
232
+ sort: "popular",
233
+ spp: "30",
234
+ brand: brandId,
235
+ },
236
+ retryOptions: { retries },
237
+ });
238
+ return res.data.data?.products ?? res.data.products ?? [];
239
+ }
240
+
241
+ /**
242
+ * It returns the data based on filters array
243
+ * @param {string} keyword - the search query
244
+ * @param {array} filters - array of filters elements like ['fbrand','fsupplier']
245
+ * @returns Total number of products
246
+ */
247
+ async searchCustomFilters(keyword, filters) {
248
+ const res = await this.session.get(Constants.URLS.SEARCH.EXACTMATCH, {
249
+ params: {
250
+ appType: Constants.APPTYPES.DESKTOP,
251
+ curr: Constants.CURRENCIES.RUB,
252
+ dest: this.dest,
253
+ lang: Constants.LOCALES.RU,
254
+ query: keyword,
255
+ resultset: "filters",
256
+ sort: "popular",
257
+ filters: filters.join(";"),
258
+ },
259
+ headers: {
260
+ "x-queryid": Utils.Search.getQueryIdForSearch(),
261
+ },
262
+ });
263
+ return res.data?.data || {};
264
+ }
265
+
266
+ /**
267
+ * It gets all products from specified page
268
+ * @param {object} catalogConfig - { keyword, catalog_type, catalog_value }
269
+ * @param {number} page - page number
270
+ * @returns {array} - An array of products
271
+ */
272
+ async getCatalogPage(catalogConfig, page = 1, retries = 0, filters = []) {
273
+ const options = {
274
+ params: {
275
+ appType: Constants.APPTYPES.DESKTOP,
276
+ curr: Constants.CURRENCIES.RUB,
277
+ dest: this.dest,
278
+ query: catalogConfig.keyword.toLowerCase(),
279
+ resultset: "catalog",
280
+ sort: "popular",
281
+ spp: "30",
282
+ suppressSpellcheck: false,
283
+ },
284
+ headers: {
285
+ "x-queryid": Utils.Search.getQueryIdForSearch(),
286
+ referrer: "https://www.wildberries.ru/catalog/0/search.aspx?page=2&sort=popular&search=" + encodeURI(catalogConfig.keyword.toLowerCase()),
287
+ },
288
+ };
289
+ if (page !== 1) {
290
+ options.params.page = page;
291
+ }
292
+ for (const filter of filters) {
293
+ options.params[filter.type] = filter.value;
294
+ }
295
+ options.retryOptions = { retries };
296
+ const res = await this.session.get(Constants.URLS.SEARCH.EXACTMATCH, options);
297
+ if (res.data?.metadata?.catalog_value === "preset=11111111") {
298
+ throw new Error("BAD CATALOG VALUE - 11111111");
299
+ }
300
+ return res.data.data?.products ?? res.data.products;
301
+ }
302
+
303
+ /**
304
+ * Search for adverts and their ads form specified keyword
305
+ * @param {string} keyword - the search query
306
+ * @returns {object} - An object with adverts and their ads
307
+ */
308
+ async getSearchAds(keyword) {
309
+ const options = { params: { keyword } };
310
+ const res = await this.session.get(Constants.URLS.SEARCH.ADS, options);
311
+ return res.data;
312
+ }
313
+
314
+ /**
315
+ * Search for carousel ads inside product card
316
+ * @param {number} productId - product id
317
+ * @returns {array} - An array with ads
318
+ */
319
+ async getCarouselAds(productId) {
320
+ const res = await this.session.get(Constants.URLS.SEARCH.CAROUSEL_ADS, {
321
+ params: { nm: productId },
322
+ });
323
+ return res.data;
324
+ }
325
+
326
+ /**
327
+ * It takes a query string and returns a list of suggestions that match the query
328
+ * @param {string} query - the search query
329
+ * @returns {array} - An array of objects.
330
+ */
331
+ async keyHint(query) {
332
+ const res = await this.session.get(Constants.URLS.SEARCH.HINT, {
333
+ params: {
334
+ query,
335
+ gender: Constants.SEX.COMMON,
336
+ locale: Constants.LOCALES.RU,
337
+ lang: Constants.LOCALES.RU,
338
+ appType: Constants.APPTYPES.DESKTOP,
339
+ },
340
+ });
341
+ return res.data;
342
+ }
343
+
344
+ /**
345
+ * It takes a productId, makes a request to the server, and returns the similar Ids
346
+ * @param productId - The product ID of the product you want to search for similar
347
+ * @returns {object} with similar product Ids
348
+ */
349
+ async searchSimilarByNm(productId) {
350
+ const res = await this.session.get(Constants.URLS.SEARCH.SIMILAR_BY_NM, {
351
+ params: { nm: productId },
352
+ });
353
+ return res.data;
354
+ }
355
+
356
+ /**
357
+ * It takes an array of productIds and a destination, and returns an array of
358
+ * products with delivery time data
359
+ * @param {array} productIds - array of product IDs
360
+ * @param {number} retries - number of retries
361
+ * @returns {array} of products with delivery times
362
+ */
363
+ async getDeliveryDataByNms(productIds, retries = 0) {
364
+ const res = await this.session.get(Constants.URLS.PRODUCT.DELIVERYDATA, {
365
+ params: {
366
+ appType: Constants.APPTYPES.DESKTOP,
367
+ locale: Constants.LOCALES.RU,
368
+ dest: this.dest,
369
+ nm: productIds.join(";"),
370
+ },
371
+ retryOptions: {
372
+ retries,
373
+ },
374
+ });
375
+ return res.data.data?.products ?? res.data.products;
376
+ }
377
+
378
+ /**
379
+ * @returns Array of promos
380
+ */
381
+ async getPromos() {
382
+ const result = await this.session.get(Constants.URLS.PROMOS);
383
+ return result.data;
384
+ }
385
+
386
+
387
+ /**
388
+ * @returns Array of found products
389
+ */
390
+ async getListOfProducts(productIds) {
391
+ const res = await this.session.get(Constants.URLS.SEARCH.LIST, {
392
+ params: {
393
+ appType: Constants.APPTYPES.DESKTOP,
394
+ dest: this.dest,
395
+ curr: Constants.CURRENCIES.RUB,
396
+ lang: Constants.LOCALES.RU,
397
+ nm: productIds.join(";"),
398
+ },
399
+ });
400
+ return res.data.data?.products ?? res.data.products ?? [];
401
+ }
402
+
403
+ /**
404
+ * @returns Object with supplier info
405
+ */
406
+ async getSupplierInfo(sellerId) {
407
+ const res = await this.session.get(format(Constants.URLS.SUPPLIER.INFO, sellerId));
408
+ return res.data || {};
409
+ }
410
+
411
+ /**
412
+ * @returns Object with supplier shipment info
413
+ */
414
+ async getSupplierShipment(sellerId) {
415
+ const res = await this.session.get(format(Constants.URLS.SUPPLIER.SHIPMENT, sellerId), {
416
+ headers: {
417
+ "x-client-name": "site",
418
+ },
419
+ });
420
+ return res.data || {};
421
+ }
422
+
423
+ /**
424
+ * It takes a supplier id and returns an array of products
425
+ * @param {number} supplierId - supplier ID
426
+ * @param {number} page - page number
427
+ * @returns {Promise<object>} - Raw API response data.
428
+ */
429
+ async getSupplierCatalog(supplierId, page = 1) {
430
+ const res = await this.session.get(Constants.URLS.SUPPLIER.CATALOG, {
431
+ params: {
432
+ appType: Constants.APPTYPES.DESKTOP,
433
+ curr: Constants.CURRENCIES.RUB,
434
+ dest: this.dest,
435
+ lang: Constants.LOCALES.RU,
436
+ page,
437
+ sort: "popular",
438
+ spp: "30",
439
+ supplier: supplierId,
440
+ },
441
+ });
442
+ return res.data || {};
443
+ }
444
+
445
+ /**
446
+ * It gets all products from supplier catalog with pagination
447
+ * @param {number} supplierId - supplier ID
448
+ * @param {number} pageCount - Number of pages to retrieve (0 = all pages)
449
+ * @param {number} retries - Number of retries for failed requests
450
+ * @returns {WBCatalog} WBCatalog object with all supplier products
451
+ */
452
+ async getSupplierCatalogAll(supplierId, pageCount = 0, retries = 0) {
453
+ const products = [];
454
+
455
+ const totalProducts = await this.getSupplierProductCount(supplierId);
456
+ if (totalProducts === 0) {
457
+ return new WBCatalog({
458
+ supplierId,
459
+ catalog_type: "supplier",
460
+ catalog_value: `supplier=${supplierId}`,
461
+ pages: 0,
462
+ products: [],
463
+ totalProducts: 0,
464
+ });
465
+ }
466
+
467
+ const catalogConfig = {
468
+ supplierId,
469
+ catalog_type: "supplier",
470
+ catalog_value: `supplier=${supplierId}`,
471
+ };
472
+
473
+ let totalPages = this.getPageCount(totalProducts);
474
+
475
+ if (pageCount > 0 && pageCount < totalPages) {
476
+ totalPages = pageCount;
477
+ }
478
+
479
+ const threads = Array.from({ length: totalPages }, (_, i) => i + 1);
480
+ const parsedPages = await mapWithConcurrency(threads, 5, (thr) => this.getSupplierCatalogPage(supplierId, thr, retries));
481
+
482
+ const productOptions = { session: this.session, destination: this.destination };
483
+ for (const page of parsedPages) {
484
+ if (Array.isArray(page)) {
485
+ products.push(...page.map((v) => new WBProduct(v, productOptions)));
486
+ }
487
+ }
488
+
489
+ Object.assign(catalogConfig, {
490
+ pages: totalPages,
491
+ products,
492
+ totalProducts,
493
+ });
494
+
495
+ return new WBCatalog(catalogConfig);
496
+ }
497
+
498
+ /**
499
+ * It gets products from specified supplier catalog page
500
+ * @param {number} supplierId - supplier ID
501
+ * @param {number} page - page number
502
+ * @param {number} retries - number of retries
503
+ * @returns {array} - An array of products
504
+ */
505
+ async getSupplierCatalogPage(supplierId, page = 1, retries = 0) {
506
+ const res = await this.session.get(Constants.URLS.SUPPLIER.CATALOG, {
507
+ params: {
508
+ appType: Constants.APPTYPES.DESKTOP,
509
+ curr: Constants.CURRENCIES.RUB,
510
+ dest: this.dest,
511
+ lang: Constants.LOCALES.RU,
512
+ page,
513
+ sort: "popular",
514
+ spp: "30",
515
+ supplier: supplierId,
516
+ },
517
+ retryOptions: { retries },
518
+ });
519
+ return res.data.data?.products ?? res.data.products ?? [];
520
+ }
521
+
522
+ getPageCount(totalProducts) {
523
+ return Math.min(Math.ceil(totalProducts / Constants.PRODUCTS_PER_PAGE), Constants.PAGES_PER_CATALOG);
524
+ }
525
+ }
526
+
527
+ module.exports = WBPrivateAPI;