Tradeinn (tradeinn.com) scraper avatar

Tradeinn (tradeinn.com) scraper

Under maintenance
Go to Store
This Actor is under maintenance.

This Actor may be unreliable while under maintenance. Would you like to try a similar Actor instead?

See alternative Actors
Tradeinn (tradeinn.com) scraper

Tradeinn (tradeinn.com) scraper

strajk/tradeinn-tradeinn-com-scraper

Scrapes products titles, prices, images and availability. Does NOT scrape product details.

Developer
Maintained by Community

Dockerfile

1FROM apify/actor-node:16
2
3COPY package.json ./
4
5RUN npm --quiet set progress=false \
6  && npm install --only=prod --no-optional
7
8COPY . ./

INPUT_SCHEMA.json

1{
2  "title": "Tradeinn (tradeinn.com) scraper",
3  "description": "Scrapes products titles, prices, images and availability. Does NOT scrape product details.",
4  "type": "object",
5  "schemaVersion": 1,
6  "properties": {
7    "mode": {
8      "title": "Mode",
9      "description": "",
10      "type": "string",
11      "editor": "select",
12      "default": "TEST",
13      "prefill": "TEST",
14      "enum": [
15        "TEST",
16        "CURATED",
17        "FULL"
18      ],
19      "enumTitles": [
20        "TEST",
21        "CURATED",
22        "FULL"
23      ]
24    },
25    "debug": {
26      "title": "Debug",
27      "description": "Debug mode prints more logs, disables concurrency and other optimizations.",
28      "type": "boolean",
29      "editor": "checkbox",
30      "default": false
31    },
32    "shop": {
33      "title": "Shop",
34      "description": "",
35      "type": "string",
36      "editor": "select",
37      "default": "BIKEINN",
38      "prefill": "BIKEINN",
39      "enum": [
40        "BIKEINN",
41        "DIVEINN",
42        "SNOWINN",
43        "TREKKINN",
44        "SMASHINN",
45        "SWIMINN",
46        "WAVEINN",
47        "MOTARDINN",
48        "OUTETINN",
49        "RUNNERINN",
50        "GOALINN",
51        "DRESSINN",
52        "TRAININN",
53        "XTREMEINN",
54        "KIDINN",
55        "TECHINN",
56        "BRICOINN"
57      ],
58      "enumTitles": [
59        "BIKEINN",
60        "DIVEINN",
61        "SNOWINN",
62        "TREKKINN",
63        "SMASHINN",
64        "SWIMINN",
65        "WAVEINN",
66        "MOTARDINN",
67        "OUTETINN",
68        "RUNNERINN",
69        "GOALINN",
70        "DRESSINN",
71        "TRAININN",
72        "XTREMEINN",
73        "KIDINN",
74        "TECHINN",
75        "BRICOINN"
76      ]
77    },
78    "APIFY_DONT_STORE_IN_DATASET": {
79      "sectionCaption": "Advanced",
80      "sectionDescription": "Advanced options, use only if you know what you're doing.",
81      "title": "Don't store in dataset",
82      "description": "If set to true, the actor will not store the results in the default dataset. Useful when using alternative storage, like own database",
83      "type": "boolean",
84      "default": false,
85      "editor": "checkbox"
86    },
87    "PG_CONNECTION_STRING_NORMALIZED": {
88      "title": "Postgres connection string for normalized data",
89      "description": "If set, actor will store normalized data in Postgres database in PG_DATA_TABLE and PG_DATA_PRICE_TABLE tables",
90      "type": "string",
91      "editor": "textfield"
92    },
93    "PG_DATA_TABLE": {
94      "title": "Postgres table name for product data",
95      "description": "Table name for storing product name, url, image, ...",
96      "type": "string",
97      "editor": "textfield"
98    },
99    "PG_DATA_PRICE_TABLE": {
100      "title": "Postgres table name for price data",
101      "description": "Table name for storing price, original price, stock status, ...",
102      "type": "string",
103      "editor": "textfield"
104    }
105  },
106  "required": [
107    "mode",
108    "shop"
109  ]
110}

apify.json

1{
2  "name": "tradeinn-tradeinn-com-scraper",
3  "version": "0.1",
4  "buildTag": "latest",
5  "env": null,
6  "defaultRunOptions": {
7    "build": "latest",
8    "timeoutSecs": 3600,
9    "memoryMbytes": 1024
10  }
11}

main.js

1/**
2 * Dev notes
3 * ===
4 *
5 * `https://www.tradeinn.com/bikeinn/en/${marca}/${id_marca}/m`
6 * - m - landing, no sorting
7 * - nm - all products, has sorting
8 *
9 * Spanish in a nutshell:
10 * marca = brand
11 * familia = family
12 * tienda = shop
13 * fecha_descatalogado = date_discontinued
14 * talla = size
15 * sostenible = sustainable
16 * tengo_tallas_traducidas = have_translated_sizes
17 * num_productes_actius = num_active_products
18 * baja = discontinued
19 * trobat = found
20 * pais = country
21 * enlace = link
22 * entrega = delivery
23 * divisa = currency
24 *
25 * TODO
26 * - v360
27 * - v180
28 * - v90
29 * - v30
30 *
31 * - brut
32 * - stock_reservat
33 * - desc_brand
34 * - pmp
35 * */
36
37// noinspection JSNonASCIINames,NonAsciiCharacters
38
39import { Actor } from "apify3";
40import { BasicCrawler, createBasicRouter } from "crawlee";
41import fetch from "node-fetch";
42import { init, save } from "./_utils/common.js";
43
44var MODE;
45
46(function (MODE) {
47  MODE["TEST"] = "TEST";
48  MODE["CURATED"] = "CURATED";
49  MODE["FULL"] = "FULL";
50})(MODE || (MODE = {}));
51var LABEL;
52
53(function (LABEL) {
54  LABEL["INDEX"] = "INDEX";
55  LABEL["PRODUCTS"] = "PRODUCTS";
56})(LABEL || (LABEL = {}));
57const BASE_URL = `https://www.tradeinn.com`;
58const PER_PAGE = 48;
59
60var SHOP;
61
62// TODO: Dry
63(function (SHOP) {
64  SHOP["BIKEINN"] = "BIKEINN";
65  SHOP["DIVEINN"] = "DIVEINN";
66  SHOP["SNOWINN"] = "SNOWINN";
67  SHOP["TREKKINN"] = "TREKKINN";
68  SHOP["SMASHINN"] = "SMASHINN";
69  SHOP["SWIMINN"] = "SWIMINN";
70  SHOP["WAVEINN"] = "WAVEINN";
71  SHOP["MOTARDINN"] = "MOTARDINN";
72  SHOP["OUTETINN"] = "OUTETINN";
73  SHOP["RUNNERINN"] = "RUNNERINN";
74  SHOP["GOALINN"] = "GOALINN";
75  SHOP["DRESSINN"] = "DRESSINN";
76  SHOP["TRAININN"] = "TRAININN";
77  SHOP["XTREMEINN"] = "XTREMEINN";
78  SHOP["KIDINN"] = "KIDINN";
79  SHOP["TECHINN"] = "TECHINN";
80  SHOP["BRICOINN"] = "BRICOINN";
81})(SHOP || (SHOP = {}));
82const SHOPS = {
83  DIVEINN: 1,
84  SNOWINN: 2,
85  TREKKINN: 3,
86  BIKEINN: 4,
87  SMASHINN: 5,
88  SWIMINN: 6,
89  WAVEINN: 7,
90  MOTARDINN: 8,
91  OUTETINN: 9,
92  RUNNERINN: 10,
93  GOALINN: 11,
94  DRESSINN: 12,
95  TRAININN: 13,
96  XTREMEINN: 14,
97  KIDINN: 15,
98  TECHINN: 16,
99  BRICOINN: 17,
100};
101
102const COUNTRIES = {
103  Albania: 1,
104  Algeria: 2,
105  "American Samoa": 3,
106  Andorra: 4,
107  Angola: 5,
108  Anguilla: 6,
109  Antigua: 7,
110  Argentina: 8,
111  Armenia: 9,
112  Aruba: 10,
113  Australia: 11,
114  Austria: 12,
115  Azerbaijan: 13,
116  Bahamas: 14,
117  Bahrain: 15,
118  Bangladesh: 16,
119  Barbados: 17,
120  Belarus: 18,
121  Belgium: 19,
122  Belize: 20,
123  Benin: 21,
124  Bermuda: 22,
125  Bhutan: 23,
126  Bolivia: 24,
127  Bonaire: 25,
128  "Bosnia and Herzegovina": 26,
129  Botswana: 27,
130  Brasil: 229,
131  Brunei: 29,
132  Bulgaria: 30,
133  "Burkina Faso": 31,
134  Burundi: 32,
135  Cambodia: 33,
136  Cameroon: 34,
137  Canada: 35,
138  "Cape Verde": 37,
139  "Cayman Islands": 38,
140  "Central African Republic": 39,
141  Chad: 40,
142  Chile: 41,
143  "China, People's Republic": 42,
144  Colombia: 43,
145  Comoros: 44,
146  Congo: 45,
147  "Congo, The Democratic Republic": 46,
148  "Cook Islands": 47,
149  "Costa Rica": 48,
150  "Cote d'Ivoire": 49,
151  Croatia: 50,
152  Cuba: 51,
153  Curacao: 52,
154  Cyprus: 53,
155  "Czech Republic, The": 54,
156  Denmark: 55,
157  Djibouti: 56,
158  Dominica: 57,
159  "Dominican Republic": 58,
160  Ecuador: 59,
161  Egypt: 60,
162  "El Salvador": 61,
163  "Equatorial Guinea": 62,
164  Eritrea: 63,
165  Estonia: 64,
166  Ethiopia: 65,
167  "Falkland Islands": 66,
168  "Faroe Islands": 67,
169  Fiji: 68,
170  Finland: 69,
171  France: 70,
172  "France - Corse": 228,
173  "France - Guadeloupe": 81,
174  "France - Martinique": 128,
175  "France - Reunion": 162,
176  "France - Tahiti": 194,
177  "French Guiana": 71,
178  Gabon: 72,
179  Gambia: 73,
180  Georgia: 74,
181  Germany: 75,
182  Ghana: 76,
183  Gibraltar: 77,
184  Greece: 78,
185  Greenland: 79,
186  Grenada: 80,
187  Guam: 82,
188  Guatemala: 83,
189  "Guinea Republic": 85,
190  "Guinea-Bissau": 86,
191  "Guyana (British)": 87,
192  Haiti: 88,
193  Honduras: 89,
194  "Hong Kong": 90,
195  Hungary: 91,
196  Iceland: 92,
197  India: 93,
198  Indonesia: 94,
199  "Iran, Islamic Republic of": 95,
200  "Ireland, Republic Of": 96,
201  Israel: 97,
202  Italy: 98,
203  Jamaica: 99,
204  Japan: 100,
205  Jordan: 102,
206  Kazakhstan: 103,
207  Kenya: 104,
208  Kiribati: 105,
209  "Korea, Republic Of": 107,
210  Kuwait: 108,
211  Kyrgyzstan: 109,
212  "Lao People's Democratic Republ": 110,
213  Latvia: 111,
214  Lebanon: 112,
215  Lesotho: 113,
216  Liberia: 114,
217  Libya: 115,
218  Liechtenstein: 116,
219  Lithuania: 117,
220  Luxembourg: 118,
221  Macau: 119,
222  "Macedonia, Republic of (FYROM)": 120,
223  Madagascar: 121,
224  Malawi: 122,
225  Malaysia: 123,
226  Maldives: 124,
227  Mali: 125,
228  Malta: 126,
229  "Marshall Islands": 127,
230  Mauritania: 129,
231  Mauritius: 130,
232  Mexico: 131,
233  "Moldova, Republic Of": 132,
234  Monaco: 133,
235  Mongolia: 134,
236  Montenegro: 227,
237  Montserrat: 135,
238  Morocco: 136,
239  Mozambique: 137,
240  Myanmar: 138,
241  Namibia: 139,
242  "Nauru, Republic Of": 140,
243  Nepal: 141,
244  "Netherlands, The": 142,
245  Nevis: 143,
246  "New Caledonia": 144,
247  "New Zealand": 145,
248  Nicaragua: 146,
249  Niger: 147,
250  Nigeria: 148,
251  Niue: 149,
252  Norway: 150,
253  Oman: 151,
254  Pakistan: 152,
255  Panama: 153,
256  "Papua New Guinea": 154,
257  Paraguay: 155,
258  Peru: 156,
259  "Philippines, The": 157,
260  Poland: 158,
261  Portugal: 159,
262  "Portugal (Madeira)": 224,
263  "Puerto Rico": 160,
264  Qatar: 161,
265  Romania: 163,
266  "Russian Federation, The": 164,
267  Rwanda: 165,
268  Saipan: 166,
269  Samoa: 167,
270  "San Marino": 226,
271  "Sao Tome and Principe": 168,
272  "Saudi Arabia": 169,
273  Senegal: 170,
274  Serbia: 225,
275  Seychelles: 171,
276  "Sierra Leone": 172,
277  Singapore: 173,
278  Slovakia: 174,
279  Slovenia: 175,
280  "Solomon Islands": 176,
281  Somalia: 177,
282  "Somaliland, Rep of (North Soma": 178,
283  "South Africa": 179,
284  Spain: 180,
285  "Spain (Canary Islands,Ceuta,Melilla)": 223,
286  "Sri Lanka": 181,
287  "St. Barthelemy": 182,
288  "St. Eustatius": 183,
289  "St. Kitts": 184,
290  "St. Lucia": 185,
291  "St. Maarten": 186,
292  "St. Vincent": 187,
293  Sudan: 188,
294  Suriname: 189,
295  Swaziland: 190,
296  Sweden: 191,
297  Switzerland: 192,
298  Syria: 193,
299  Taiwan: 195,
300  Tajikistan: 196,
301  Tanzania: 197,
302  Thailand: 198,
303  Togo: 199,
304  Tonga: 200,
305  "Trinidad and Tobago": 201,
306  Tunisia: 222,
307  Turkey: 202,
308  Turkmenistan: 203,
309  "Turks and Caicos Islands": 204,
310  Tuvalu: 205,
311  Uganda: 206,
312  Ukraine: 207,
313  "United Arab Emirates": 208,
314  "United Kingdom": 209,
315  "United Kingdom (Guernsey)": 84,
316  "United Kingdom (Jersey)": 101,
317  "United States Of America": 210,
318  Uruguay: 211,
319  Uzbekistan: 212,
320  Vanuatu: 213,
321  Vietnam: 215,
322  "Virgin Islands (British)": 216,
323  "Virgin Islands (US)": 217,
324  Yemen: 218,
325  Zambia: 220,
326  Zimbabwe: 221,
327};
328
329// [...$('.imagenesLogos .marca_line')].reduce((acc, x) => { acc[x.querySelector('img').alt] = x.id; return acc}, {})
330const BRANDS = {
331  "+8000": `586`,
332  "100percent": `1275`,
333  "10bar": `1110`,
334  "226ERS": `1360`,
335  "2Toms": `5436`,
336  "2XU": `1283`,
337  "3M": `1362`,
338  "3t": `8734`,
339  "4arm Strong": `3934`,
340  "4F": `7566`,
341  "4iiii": `3426`,
342  "5 five": `8109`,
343  "8 C Plus": `2190`,
344  "9transport": `7829`,
345  Abloc: `8537`,
346  "Absolute Black": `4878`,
347  ABUS: `1363`,
348  Acid: `8077`,
349  Acimut: `7786`,
350  Acme: `4541`,
351  "Action Outdoor": `2017`,
352  adidas: `263`,
353  "Adidas Badminton": `5356`,
354  "adidas originals": `646`,
355  AEE: `368`,
356  Aftershokz: `1120`,
357  Afton: `3381`,
358  Agfa: `5490`,
359  AGU: `6478`,
360  Aifeit: `7033`,
361  "Air Relax": `5962`,
362  Airbone: `4473`,
363  Airman: `5734`,
364  "Airn Outdoor": `9681`,
365  Airofit: `8544`,
366  Airpop: `6916`,
367  Airwings: `5160`,
368  Alhonga: `7739`,
369  "All Mountain Style": `7516`,
370  "All Sins 18k": `9061`,
371  Allmatters: `8891`,
372  "Alpcross Components": `4884`,
373  Alpenheat: `439`,
374  "Alpha Industries": `6700`,
375  Alpina: `2379`,
376  Alpine: `5736`,
377  Alpinestars: `335`,
378  "Alpinestars Bicycle": `9606`,
379  Alpitude: `7737`,
380  "Altec Lansing": `4483`,
381  Alé: `1697`,
382  Amazfit: `4414`,
383  "American Classic": `3016`,
384  Amiaud: `4862`,
385  Amix: `4486`,
386  Amkov: `7563`,
387  Amlsport: `1020`,
388  Amplifi: `3679`,
389  "An Lun": `5282`,
390  "Ana Maria Lajusticia": `2874`,
391  Andrys: `7787`,
392  "Angelina Calzino": `8526`,
393  Anlen: `5281`,
394  Ansmann: `1112`,
395  Aphex: `7482`,
396  Apple: `4321`,
397  Approx: `5893`,
398  Apurna: `8735`,
399  Aqua2go: `7113`,
400  Aquafeel: `7310`,
401  Aquas: `2265`,
402  "Arch Max": `1235`,
403  "Arc’teryx": `1222`,
404  Arena: `11`,
405  Arexons: `10440`,
406  "Argon 18": `6061`,
407  Ariete: `1723`,
408  Arisun: `4970`,
409  Armada: `418`,
410  "Armor-x": `1293`,
411  Artein: `10439`,
412  Artsana: `6812`,
413  Arundel: `8534`,
414  Arva: `276`,
415  Ashima: `2896`,
416  Asics: `284`,
417  Asista: `3609`,
418  Askoll: `8514`,
419  "Ass Savers": `3670`,
420  Assos: `13`,
421  Astvte: `4158`,
422  Asus: `4382`,
423  Así: `8889`,
424  Atala: `4903`,
425  "ATC LTD TA": `4937`,
426  Atipick: `2613`,
427  Atv: `8547`,
428  Auvray: `4978`,
429  Avento: `3622`,
430  Avid: `16`,
431  "Awesome Maps": `7367`,
432  AXA: `3664`,
433  Axxios: `7405`,
434  "B&W": `5432`,
435  "B-Race": `7791`,
436  "B-urban": `7792`,
437  Babolat: `18`,
438  "Back On Track": `8738`,
439  Barbieri: `3687`,
440  Barfly: `4789`,
441  Barts: `2467`,
442  Basil: `2699`,
443  BBB: `483`,
444  Beal: `3430`,
445  Bear: `5430`,
446  "Bearing Cw": `7788`,
447  Beeline: `7118`,
448  Beeloom: `9598`,
449  Bell: `286`,
450  "Bell Italy": `7789`,
451  "Bella Aurora": `1874`,
452  Bellelli: `2045`,
453  Bematrix: `8802`,
454  Bend36: `3904`,
455  Benlee: `1644`,
456  Berghaus: `24`,
457  Bergner: `8144`,
458  Bering: `1338`,
459  Bern: `25`,
460  Beryl: `7119`,
461  "Best Divers": `26`,
462  Beto: `3773`,
463  Bettr: `6980`,
464  Beurer: `5520`,
465  "Bh Fitness": `7120`,
466  "Bicis Esteve": `7032`,
467  Bicisupport: `3890`,
468  "Bicycle Line": `1958`,
469  Biemme: `8936`,
470  "Bike Ahead": `9185`,
471  "Bike Fashion": `4476`,
472  "Bike Hand": `2897`,
473  "Bike Workx": `4803`,
474  "Bike Yoke": `8739`,
475  Bike7: `8741`,
476  Bikecare: `2773`,
477  Bikefinder: `9576`,
478  Bikefun: `3396`,
479  Bikeinn: `1272`,
480  Bikeribbon: `2601`,
481  "Bikers Dream": `5163`,
482  "Bikers Own": `4749`,
483  Bikeshield: `7510`,
484  Biknd: `7455`,
485  Billow: `5904`,
486  Bimanan: `9841`,
487  Bimpair: `5437`,
488  Biofreeze: `6905`,
489  Biona: `6981`,
490  Bioracer: `5647`,
491  "Biotech Usa": `9831`,
492  Biotex: `3932`,
493  Biotherm: `4054`,
494  Biotonik: `7887`,
495  Birzman: `7504`,
496  Bistark: `7790`,
497  "Black Bearing": `8542`,
498  "Black Cat Tire": `7738`,
499  "Black Diamond": `643`,
500  "Black Inc": `5008`,
501  Blackburn: `283`,
502  Blackspire: `7503`,
503  Blindsave: `8768`,
504  Bliz: `5486`,
505  Blossum: `4672`,
506  Blub: `4828`,
507  "Blueball Sport": `4304`,
508  Bluegrass: `875`,
509  "BnB Rack": `3921`,
510  Bobike: `534`,
511  "Body Glide": `1510`,
512  Bodygood: `9001`,
513  Bolle: `2550`,
514  Bombshell: `8743`,
515  Bombtrack: `4904`,
516  Bompar: `3891`,
517  "Bone Collection": `8558`,
518  Bonin: `2913`,
519  Bont: `3816`,
520  Bookman: `8935`,
521  Born: `1536`,
522  "Born Fruits": `1938`,
523  "Born Living Yoga": `6778`,
524  Bosch: `4477`,
525  "Boston Golf": `9277`,
526  Bottecchia: `8853`,
527  Box: `8744`,
528  Br: `6850`,
529  Brakco: `7035`,
530  Brandit: `6794`,
531  Braun: `5470`,
532  Breezer: `5277`,
533  Bresser: `6088`,
534  Brigmton: `5524`,
535  Briko: `466`,
536  "Britax Römer": `3546`,
537  "Brooks England": `1701`,
538  Brose: `8996`,
539  Broyx: `4849`,
540  Brunox: `4478`,
541  Bryton: `896`,
542  Bta: `10458`,
543  "Bub-up": `8745`,
544  Buchel: `4479`,
545  Buddyswim: `3194`,
546  "Buff ®": `252`,
547  "Build Your Brand": `10328`,
548  Bulls: `9200`,
549  Burgtec: `4933`,
550  Burley: `4653`,
551  Burton: `3647`,
552  "Busch&Muller": `4686`,
553  Buzzrack: `6234`,
554  "Bv Sport": `1344`,
555  Byte: `7471`,
556  Cairn: `3035`,
557  Camelbak: `36`,
558  Campagnolo: `37`,
559  Campingaz: `2175`,
560  "Cane Creek": `8292`,
561  Cannondale: `1242`,
562  Canyon: `4851`,
563  Capgo: `7762`,
564  Casall: `1282`,
565  Castelli: `497`,
566  Cateye: `40`,
567  Catlike: `41`,
568  "Cayler & Sons": `8649`,
569  Cebe: `2442`,
570  Cecotec: `4998`,
571  Cegasa: `6645`,
572  Celly: `2591`,
573  Cep: `1627`,
574  Ceramicspeed: `3702`,
575  "Cerda Group": `4926`,
576  Challenge: `3785`,
577  "Chamois Butt´r": `4532`,
578  Chaoyang: `3407`,
579  "Charge Sports Drinks": `8349`,
580  Chase: `8771`,
581  Chiba: `5164`,
582  Chicco: `6649`,
583  Chimpanzee: `1679`,
584  "Chris King": `4936`,
585  Chrome: `5003`,
586  Cinelli: `528`,
587  Cinq: `3876`,
588  Citadel: `5648`,
589  Citybug: `2346`,
590  "Cl Brakes": `7449`,
591  Classic: `5377`,
592  Clever: `4779`,
593  Clicgear: `8772`,
594  Clif: `1339`,
595  "Climbing Technology": `2578`,
596  Clinique: `3998`,
597  Clipon: `9171`,
598  Closca: `5646`,
599  Closethegap: `4885`,
600  "Club Ride": `4485`,
601  CMP: `640`,
602  cnSPOKE: `5283`,
603  Coas: `6747`,
604  Cobags: `7129`,
605  Cocoon: `9120`,
606  "Codex-U": `4363`,
607  Cofra: `6659`,
608  "Color Baby": `6832`,
609  Coluer: `6005`,
610  Columbia: `347`,
611  Columbus: `594`,
612  Compeed: `4194`,
613  Compex: `617`,
614  Composite: `8548`,
615  Compressport: `47`,
616  Conor: `8082`,
617  Consumo: `5058`,
618  Contact: `5529`,
619  Contec: `8969`,
620  Continental: `48`,
621  Controltech: `5950`,
622  Coolbox: `4334`,
623  Coospo: `10357`,
624  Copa: `3074`,
625  Coros: `4487`,
626  Corratec: `2235`,
627  Corsurf: `9454`,
628  "Cosmo Connected": `4719`,
629  CPA: `2594`,
630  Craft: `50`,
631  Craghoppers: `3078`,
632  Crane: `3780`,
633  Crankbrothers: `282`,
634  Cratoni: `3524`,
635  "Crazy Safety": `1943`,
636  "Crep Protect": `2859`,
637  Cressi: `51`,
638  Croozer: `3084`,
639  Crosscall: `4401`,
640  Crud: `5964`,
641  CST: `4720`,
642  Cube: `52`,
643  Cutered: `10039`,
644  Cybex: `3836`,
645  Cycl: `7132`,
646  "Cycling Ceramic": `3447`,
647  Cyclo: `2703`,
648  Cycloc: `5965`,
649  Cycology: `7922`,
650  "D-Light": `3889`,
651  Daewoo: `3193`,
652  Dahon: `384`,
653  Dainese: `346`,
654  Dakar: `7728`,
655  Dakine: `54`,
656  Damartsport: `5644`,
657  Dane: `4540`,
658  Dare2B: `2945`,
659  Darevie: `4902`,
660  Darkpads: `5429`,
661  Dashel: `6324`,
662  "Dcu Tecnologic": `5863`,
663  Decleor: `4055`,
664  Deda: `3674`,
665  Deerhunter: `6002`,
666  Deestone: `2899`,
667  Defeet: `2914`,
668  Deli: `2900`,
669  "Delta Cycle": `3432`,
670  Denver: `5743`,
671  Deportium: `7484`,
672  Dexshell: `8970`,
673  Dickies: `7008`,
674  Difi: `6067`,
675  "Dirt Freak": `7847`,
676  Disney: `2568`,
677  "Dkn Technology": `9599`,
678  Dmr: `8540`,
679  DMT: `597`,
680  Dom: `3802`,
681  Dosun: `2918`,
682  Douchebags: `613`,
683  "Dr Cool": `2139`,
684  "Dr Senst": `6258`,
685  "Dr. Organic": `6726`,
686  Drasanvi: `7473`,
687  Drift: `4467`,
688  "Drop Shot": `372`,
689  Drwheeler: `6909`,
690  "Ds Covers": `7435`,
691  "DT Swiss": `2173`,
692  Duracell: `2962`,
693  "Dutch Perfect": `2595`,
694  DVO: `6704`,
695  Dyedbro: `4934`,
696  Dynafit: `65`,
697  "E-Bike Vision": `4618`,
698  "E-thirteen": `5154`,
699  Eafit: `9094`,
700  Eassun: `887`,
701  Easton: `2034`,
702  Eastpak: `411`,
703  Easycamp: `3275`,
704  Easypix: `2282`,
705  EBC: `7500`,
706  Ebon: `4886`,
707  Echowell: `2901`,
708  Ecoflow: `6928`,
709  "Econic One": `7044`,
710  Ecoon: `9439`,
711  Edelrid: `281`,
712  Edm: `8072`,
713  "Effetto Mariposa": `9031`,
714  Eines: `9397`,
715  "El Gallo": `4837`,
716  Elevn: `8654`,
717  Elite: `69`,
718  Elix: `7890`,
719  Eloa: `6983`,
720  Eltin: `2954`,
721  Elvedes: `7793`,
722  Emhome: `7685`,
723  Enduro: `7501`,
724  "Enduro Bearings": `8639`,
725  Eneloop: `6262`,
726  Energizer: `1475`,
727  "Energy Sistem": `4402`,
728  Enervit: `4725`,
729  "Enforma Socks": `4986`,
730  Enve: `8774`,
731  Eolos: `1346`,
732  Eovolt: `8761`,
733  Epitact: `8775`,
734  Epoch: `3888`,
735  Epsealon: `1480`,
736  Ergon: `3015`,
737  Ergotec: `4621`,
738  Erima: `8621`,
739  Errea: `8625`,
740  Esge: `4722`,
741  ESIgrips: `4793`,
742  Esperia: `8852`,
743  Etixx: `3752`,
744  Etxeondo: `644`,
745  Eucerin: `3986`,
746  Everactive: `9330`,
747  Eveready: `2963`,
748  Evergy: `9428`,
749  "Evo Bikes": `8801`,
750  Evoc: `7494`,
751  Evolve: `8655`,
752  "Exa Form": `7411`,
753  Exal: `4656`,
754  "Excellent Houseware": `8140`,
755  Excess: `8776`,
756  "Exposure Lights": `4787`,
757  Exustar: `2385`,
758  "F-Lite": `4689`,
759  F100: `4687`,
760  "Fabio Quartararo": `2848`,
761  Fabric: `1996`,
762  "Fabrica De Juguetes Chicos": `6959`,
763  Fahrer: `4628`,
764  Falke: `1148`,
765  "Far&Near": `3678`,
766  Fashy: `7309`,
767  Fasi: `4939`,
768  "Fast Forward": `9076`,
769  Fastrider: `7673`,
770  Favero: `7001`,
771  Favour: `6112`,
772  FDP: `7897`,
773  Federal: `8778`,
774  Feedback: `3019`,
775  "Feelfree Gear": `3376`,
776  Fenix: `2395`,
777  Ferrino: `73`,
778  FFWD: `4742`,
779  Fidlock: `3762`,
780  "Fil Safe": `5431`,
781  Fila: `1533`,
782  Finis: `261`,
783  "Finish Line": `2605`,
784  Finna: `4276`,
785  First: `3902`,
786  "First Bike": `2372`,
787  "Fischer Bikes": `6004`,
788  "Fisio Xtreme": `9284`,
789  Fisiocrem: `581`,
790  Fitbit: `1613`,
791  "Fitfiu Fitness": `9105`,
792  "Fitness Tech": `8998`,
793  "Five Ten": `2367`,
794  Fixplus: `3132`,
795  Fizik: `302`,
796  Flashmer: `1306`,
797  Flectr: `3147`,
798  Flexall: `6906`,
799  Flexir: `7462`,
800  FLM: `2781`,
801  "Fly Racing": `7453`,
802  Focus: `8907`,
803  Force: `7036`,
804  "Force Xv": `5637`,
805  Formigli: `7741`,
806  Formula: `3871`,
807  Forward: `8782`,
808  Fox: `3052`,
809  "Fox Rage": `5940`,
810  Foxman: `2177`,
811  Fpd: `8804`,
812  FSA: `2050`,
813  Fuji: `988`,
814  "Fuji-toki": `7773`,
815  Fulcrum: `76`,
816  FullGas: `3782`,
817  "Fun Bike": `8359`,
818  Funken: `9307`,
819  Funkita: `609`,
820  "Funky Trunks": `610`,
821  Fytter: `573`,
822  "G-kos": `7794`,
823  "G-Star": `1914`,
824  Gaadi: `4629`,
825  Gaiam: `1913`,
826  Galfer: `2704`,
827  Garcia: `6006`,
828  Garibaldi: `1518`,
829  "Gear Aid": `3386`,
830  Geco: `3906`,
831  Gembird: `6504`,
832  Gen: `7525`,
833  "Genuine Innovations": `3826`,
834  Geosmina: `10040`,
835  GES: `2902`,
836  GHOST: `4854`,
837  Gigabyte: `4425`,
838  Gios: `6727`,
839  Gipiemme: `9790`,
840  Giro: `81`,
841  Gist: `4726`,
842  Givi: `6706`,
843  Givova: `7492`,
844  Giyo: `2919`,
845  Gladiatorfit: `9797`,
846  Globber: `8617`,
847  Globus: `6480`,
848  "Go System": `3139`,
849  "Goal Zero": `5447`,
850  "Gold Nutrition": `6737`,
851  Goldfren: `9809`,
852  Goobay: `5894`,
853  Goodyear: `1645`,
854  GoPro: `84`,
855  "GORE® Wear": `3158`,
856  "Gorilla Sports": `9615`,
857  "Gorilla Tape": `7759`,
858  Gp: `8205`,
859  "Gp Batteries": `3848`,
860  Graff: `5588`,
861  "Granite Design": `6722`,
862  "Green Cell": `9564`,
863  Gregory: `1010`,
864  Gridinlux: `9671`,
865  Grifone: `1276`,
866  GripGrab: `2275`,
867  Grundens: `6386`,
868  GT: `3393`,
869  GTR: `4993`,
870  GU: `1284`,
871  "Gu Energy": `9239`,
872  Guardian: `1564`,
873  Guee: `7499`,
874  Gurpil: `3313`,
875  "Guy Harvey": `471`,
876  Gymstick: `5454`,
877  H4u: `9824`,
878  Haberland: `4690`,
879  HAD: `4657`,
880  Haeger: `6356`,
881  Haglöfs: `371`,
882  Haibike: `4622`,
883  Hamax: `3548`,
884  Handlz: `4994`,
885  Handup: `5457`,
886  Hannah: `2055`,
887  Hape: `7164`,
888  "Hapo-g": `7795`,
889  Harbinger: `7551`,
890  Haro: `2719`,
891  "Hart Hunting": `3684`,
892  Hartex: `8382`,
893  "Hawaiian Tropic": `5075`,
894  Head: `88`,
895  "Head Bike": `9820`,
896  "Head Swimming": `3705`,
897  Headgy: `4979`,
898  Hebie: `4610`,
899  Hedkayse: `8511`,
900  Heidenau: `4630`,
901  Held: `500`,
902  Helite: `8510`,
903  Hellfire: `2783`,
904  "Helly Hansen": `90`,
905  Hergo: `3893`,
906  Herrmans: `4611`,
907  Herschel: `3002`,
908  Hibros: `7892`,
909  Hilx: `7785`,
910  Hiplok: `2761`,
911  Hirzl: `4887`,
912  Hivital: `8339`,
913  HJC: `503`,
914  "Ho Soccer": `2052`,
915  Hock: `4631`,
916  Hollis: `92`,
917  Homcom: `7700`,
918  Honor: `4341`,
919  Hoopoe: `4815`,
920  Hope: `7498`,
921  Horn: `4724`,
922  "Hotspot Design": `2076`,
923  Hovding: `7169`,
924  Hsn: `8560`,
925  HT: `3316`,
926  "Htp Design": `7468`,
927  Huawei: `3180`,
928  "Huck Norris": `9165`,
929  Hummel: `5860`,
930  Hutchinson: `93`,
931  HydraKnight: `4804`,
932  Hydroponic: `2222`,
933  "Hygen Spray": `7893`,
934  Hyperice: `9087`,
935  Ibera: `2197`,
936  Ibis: `7883`,
937  Icebreaker: `1670`,
938  Icepeak: `6802`,
939  IceToolz: `5570`,
940  "Id Match": `8324`,
941  Igpsport: `9577`,
942  Ihealth: `2398`,
943  Iluv: `1916`,
944  Impac: `4658`,
945  "Impala Rollers": `4830`,
946  Imperial: `4554`,
947  "Inca Hair Jewellery": `6963`,
948  "Industry Nine": `4794`,
949  Infini: `4693`,
950  "Infinitii Ramps": `8947`,
951  Infinity: `2359`,
952  "Inno Bike": `4694`,
953  Innova: `2903`,
954  "Innova Nutrition": `5019`,
955  Inpeak: `10036`,
956  Insight: `8646`,
957  Inspyre: `8971`,
958  Insta360: `5504`,
959  Intenso: `2062`,
960  "Interphone Cellularline": `1115`,
961  Intova: `98`,
962  Inxide: `6720`,
963  ION: `1946`,
964  Iprotec: `1732`,
965  "iQ-Company": `316`,
966  "Iron-ic": `7030`,
967  ISB: `3766`,
968  Isbjörn: `1211`,
969  ISM: `2021`,
970  Iswari: `9840`,
971  ITM: `599`,
972  Ixcor: `9745`,
973  Ixon: `2455`,
974  iXS: `2456`,
975  Izas: `510`,
976  "Izumi Chain": `4256`,
977  "J.Juan": `3894`,
978  Jagwire: `2051`,
979  Jako: `8622`,
980  Janod: `5339`,
981  Jata: `6359`,
982  JCOOL: `4899`,
983  JeansTrack: `3959`,
984  Jeep: `7697`,
985  "Jetblack Cycling": `7099`,
986  Joby: `3961`,
987  "JOE´S": `2546`,
988  "John Smith": `1736`,
989  Joluvi: `4684`,
990  Joma: `607`,
991  "Jopa Mx": `10557`,
992  JRC: `463`,
993  Jucad: `9245`,
994  "Juice Lubes": `4786`,
995  Julbo: `369`,
996  JVC: `4557`,
997  "K-Edge": `1617`,
998  "K-Swiss": `445`,
999  "K-up": `8631`,
1000  Kalas: `9746`,
1001  Kali: `332`,
1002  "Kali Protectives": `4315`,
1003  Kalloy: `3928`,
1004  Kanebo: `4258`,
1005  Kappa: `1681`,
1006  Kaps: `9182`,
1007  "Kari Traa": `7077`,
1008  Kariban: `8623`,
1009  Karpos: `867`,
1010  Kask: `1232`,
1011  Kayak: `5171`,
1012  Kazam: `3125`,
1013  KCNC: `3651`,
1014  Keboo: `9590`,
1015  Ked: `8609`,
1016  Keen: `102`,
1017  Keep: `490`,
1018  Kelme: `2329`,
1019  Kempa: `1337`,
1020  Kenda: `2036`,
1021  Kengine: `7037`,
1022  Kenny: `6891`,
1023  "Keto Protein": `9828`,
1024  "Kids Licensing": `5359`,
1025  Kidzamo: `8065`,
1026  Kilpi: `1562`,
1027  Kimood: `8637`,
1028  "Kind Shock": `2863`,
1029  Kinetic: `3818`,
1030  "Klan-E": `4504`,
1031  KLICKfix: `3754`,
1032  Klim: `4819`,
1033  Klower: `7889`,
1034  Klättermusen: `6806`,
1035  KMC: `2706`,
1036  Knog: `430`,
1037  Kodak: `4412`,
1038  Kody: `4839`,
1039  Kokua: `2548`,
1040  "KOM Cycling": `5448`,
1041  Kong: `353`,
1042  KOO: `3201`,
1043  "Kookie Cat": `6984`,
1044  Korda: `8911`,
1045  Koss: `4559`,
1046  Kovix: `5718`,
1047  Krafwin: `1114`,
1048  Krf: `3256`,
1049  Kroon: `8706`,
1050  Kruskis: `400`,
1051  Kryptonite: `2720`,
1052  "KS Tools": `6143`,
1053  KSIX: `1351`,
1054  "KT Tape": `4310`,
1055  Ktm: `8314`,
1056  Kujo: `5722`,
1057  Kynay: `8980`,
1058  Kyrocream: `1023`,
1059  "L-twoo": `10332`,
1060  L2s: `8916`,
1061  "La Sportiva": `482`,
1062  Lacd: `7832`,
1063  "Lacomed Sport": `7796`,
1064  Lacoste: `645`,
1065  Lactojoy: `8550`,
1066  Lafuma: `305`,
1067  Laica: `7189`,
1068  Lake: `1464`,
1069  Lalizas: `401`,
1070  Lancaster: `4159`,
1071  "Lasalle Sports": `7467`,
1072  Lavina: `7894`,
1073  Lazer: `1937`,
1074  "Le Coq Sportif": `2407`,
1075  Leatt: `1281`,
1076  "Led Lenser": `107`,
1077  "Legend Ebikes": `9326`,
1078  Legnano: `7027`,
1079  Lelumia: `8535`,
1080  Lemond: `1019`,
1081  Lenz: `628`,
1082  "Leonardi Racing": `8708`,
1083  Leone1947: `4734`,
1084  Leotec: `4667`,
1085  Lezyne: `110`,
1086  LG: `4342`,
1087  Lidergrip: `9393`,
1088  LifeSystems: `1662`,
1089  Lifeventure: `1663`,
1090  Limar: `112`,
1091  Linka: `5280`,
1092  Livall: `2927`,
1093  Livecell: `9843`,
1094  Lizard: `3094`,
1095  "Lizard Skins": `8918`,
1096  Lockbox: `6892`,
1097  Loeffler: `2866`,
1098  Lof: `1737`,
1099  Logan: `4889`,
1100  Lolë: `3444`,
1101  Lonsdale: `1646`,
1102  Look: `114`,
1103  Louri: `8610`,
1104  Luft: `5373`,
1105  Luma: `971`,
1106  Lumineo: `8168`,
1107  Luminox: `2049`,
1108  "Lumos Helmet": `8513`,
1109  Lupine: `280`,
1110  "M-Wave": `4751`,
1111  "Mac In A Sac": `6008`,
1112  Mach1: `8052`,
1113  Macna: `2088`,
1114  Macron: `8620`,
1115  Mader: `9363`,
1116  Madform: `1343`,
1117  Madwave: `2383`,
1118  "Mag-Lite": `621`,
1119  Magene: `7880`,
1120  "Magic Shine": `3781`,
1121  Magped: `8920`,
1122  Magura: `2204`,
1123  Mako: `545`,
1124  Mammut: `120`,
1125  Manitou: `8564`,
1126  "Marc Marquez": `976`,
1127  Marmot: `122`,
1128  Marshguard: `8536`,
1129  Maruni: `8281`,
1130  Marzocchi: `3633`,
1131  MASSI: `2144`,
1132  "Master Lock": `1580`,
1133  "Matcha & Co": `9836`,
1134  Matt: `520`,
1135  Maurten: `7906`,
1136  Mavic: `124`,
1137  "Max Protein": `9848`,
1138  Maxcom: `2063`,
1139  Maxell: `1038`,
1140  Maxim: `7495`,
1141  Maxxis: `125`,
1142  "MB Wear": `5147`,
1143  Mbm: `2718`,
1144  "Mc David": `3797`,
1145  McNett: `126`,
1146  Mebaline: `2886`,
1147  Medisana: `3181`,
1148  Megamo: `2937`,
1149  Meilan: `9580`,
1150  Melon: `3525`,
1151  Menabo: `2236`,
1152  "Mercury Equipment": `3051`,
1153  "Merlin Bike Care": `4251`,
1154  Messingschlager: `4613`,
1155  MET: `129`,
1156  Metalsub: `2094`,
1157  Mfi: `8973`,
1158  Miche: `2145`,
1159  Michelin: `264`,
1160  MicroSHIFT: `4867`,
1161  Midland: `130`,
1162  Mighty: `5284`,
1163  "Mijnen Pieper": `4640`,
1164  Mikasa: `2465`,
1165  "Miles Wide": `4783`,
1166  Milkit: `4946`,
1167  Minoura: `535`,
1168  Mission: `1574`,
1169  "Mister Tee": `8627`,
1170  Mitas: `2596`,
1171  Mizuno: `132`,
1172  Mks: `6387`,
1173  Mobilis: `4337`,
1174  Mobius: `3788`,
1175  Momabikes: `3621`,
1176  Momum: `8532`,
1177  Mondaine: `2948`,
1178  "Monkeys Sauce": `4840`,
1179  Montane: `1055`,
1180  Montbell: `5317`,
1181  Montura: `4746`,
1182  Moon: `2904`,
1183  Mooquer: `9387`,
1184  "Moose Soft-goods": `4260`,
1185  "Morgan Blue": `3182`,
1186  Mosconi: `2710`,
1187  Mota: `8178`,
1188  Motip: `7696`,
1189  Motorex: `6689`,
1190  Motorola: `2180`,
1191  Mounty: `7766`,
1192  "Mr. Wolf": `8608`,
1193  "Mr.control": `7727`,
1194  MRP: `3784`,
1195  MSC: `2864`,
1196  "MTB Hopper": `7434`,
1197  "Muc Off": `1971`,
1198  Mueller: `394`,
1199  "Mund Socks": `2264`,
1200  Musto: `619`,
1201  Muvi: `648`,
1202  Muvit: `2920`,
1203  "Muvit Io": `8682`,
1204  Myflash: `7210`,
1205  Mykronoz: `2928`,
1206  Myn: `9750`,
1207  MyWay: `2929`,
1208  Nalini: `5947`,
1209  "Named Sport": `3710`,
1210  Nathan: `962`,
1211  Natruly: `6985`,
1212  "Natural Fit": `8255`,
1213  Naturalshine: `137`,
1214  "Natures Bounty": `6725`,
1215  Naturtierra: `9832`,
1216  Navali: `3895`,
1217  "NC-17": `7511`,
1218  Nebbia: `6047`,
1219  "Nebo Tools": `1733`,
1220  Neco: `4805`,
1221  Neparo: `6271`,
1222  Net: `2039`,
1223  "New Balance": `1312`,
1224  "New Era": `2891`,
1225  "New Looxs": `2373`,
1226  Newline: `5300`,
1227  Newton: `880`,
1228  Nexgim: `9780`,
1229  Nextorch: `977`,
1230  Nfun: `2911`,
1231  Nike: `365`,
1232  "Nike Swim": `2770`,
1233  Nilox: `554`,
1234  Nimo: `8135`,
1235  Niner: `3908`,
1236  "Nite Ize": `1906`,
1237  "Nite Rider": `4741`,
1238  Nokia: `3406`,
1239  Nonbak: `2769`,
1240  Nooyah: `4900`,
1241  Norco: `4697`,
1242  Northwave: `139`,
1243  Novatec: `5319`,
1244  Nox: `140`,
1245  Nrc: `10467`,
1246  NRG: `7798`,
1247  "Nu Swimrun": `4975`,
1248  Nutcase: `1573`,
1249  Nutrinovex: `8509`,
1250  Nutrisport: `1028`,
1251  Nuvo: `3931`,
1252  "O-stand": `6388`,
1253  Oakley: `142`,
1254  "Ocean Sunglasses": `2302`,
1255  Oceanarium: `5443`,
1256  Oceanic: `144`,
1257  Ocun: `1289`,
1258  Odeclas: `1513`,
1259  ODI: `3783`,
1260  Odlo: `145`,
1261  Odyssey: `8795`,
1262  Oem: `7220`,
1263  OJ: `2363`,
1264  Oko: `8943`,
1265  Olive: `4817`,
1266  Ology: `2142`,
1267  One: `7769`,
1268  "One Industries": `3808`,
1269  Oneal: `141`,
1270  OnGuard: `1917`,
1271  "Only Play": `7458`,
1272  "Onn Style": `2608`,
1273  Onza: `7497`,
1274  Oppo: `4981`,
1275  "Orange Mud": `5865`,
1276  "Orange Seal": `4831`,
1277  Orbegozo: `5481`,
1278  Orca: `5933`,
1279  Orcatorch: `1581`,
1280  "Oregon Scientific": `150`,
1281  Oreka: `4841`,
1282  Orontas: `4157`,
1283  Ortlieb: `1225`,
1284  Osprey: `883`,
1285  Otso: `4846`,
1286  Ottolock: `6409`,
1287  "Out Of": `6927`,
1288  "Outdoor Research": `982`,
1289  Outwell: `3280`,
1290  Overade: `5320`,
1291  Overstims: `6795`,
1292  Oxford: `6693`,
1293  Oxypro: `9000`,
1294  "O´neill": `926`,
1295  "P.A.C.": `4698`,
1296  "Pacific Socks": `8695`,
1297  Paingone: `6172`,
1298  Paleobull: `9822`,
1299  Palomar: `7229`,
1300  Panaracer: `536`,
1301  Panasonic: `3318`,
1302  Pangea: `7459`,
1303  Panzer: `9096`,
1304  "Panzer Glass": `4962`,
1305  "Park Tool": `3828`,
1306  Pax: `4879`,
1307  Peak: `8633`,
1308  "Peak Performance": `367`,
1309  "Pearl Izumi": `537`,
1310  "Peaty´s": `4932`,
1311  "Pedal Plate": `5869`,
1312  "Pedro´s": `3020`,
1313  Pelagic: `1008`,
1314  Pembree: `9281`,
1315  "Pepe Jeans": `1588`,
1316  "Perfect Nutrition": `8995`,
1317  Peruzzo: `2041`,
1318  "Petrol Industries": `3699`,
1319  "Petunia Pickle Bottom": `4777`,
1320  Philips: `2065`,
1321  Phorm: `4154`,
1322  Pieces: `7422`,
1323  Pieich: `3076`,
1324  Pilo: `4494`,
1325  Pinarello: `8699`,
1326  Pintyplus: `7237`,
1327  Pioneer: `4570`,
1328  Pirelli: `3606`,
1329  Pissei: `5438`,
1330  Pivo: `7062`,
1331  Platinet: `7239`,
1332  Pletscher: `4699`,
1333  PNI: `6989`,
1334  PNK: `7755`,
1335  POC: `159`,
1336  Point: `4614`,
1337  "Point 65": `4735`,
1338  Pokal: `3930`,
1339  Polar: `160`,
1340  "Polar Gran Fondo": `1229`,
1341  "Polaris Bikewear": `1743`,
1342  Polaroid: `3185`,
1343  "Polaroid Eyewear": `1739`,
1344  Polini: `5453`,
1345  Polisport: `3547`,
1346  "Position One": `8924`,
1347  "POV-Case": `634`,
1348  "Power Plate": `8940`,
1349  Powerbar: `163`,
1350  Powergym: `1481`,
1351  Powershot: `4871`,
1352  Praxis: `3144`,
1353  "Predio Son Quint": `9851`,
1354  "Pride Racing": `8656`,
1355  Prince: `167`,
1356  PRO: `442`,
1357  "Pro Action": `1629`,
1358  "Pro Feet": `7764`,
1359  "Pro Line": `4700`,
1360  Proact: `8794`,
1361  Procell: `2879`,
1362  "Profile Design": `6740`,
1363  Progress: `3410`,
1364  Progrip: `7799`,
1365  Prologo: `170`,
1366  Promax: `4752`,
1367  Proquimia: `5458`,
1368  Protap: `9308`,
1369  "Protein Gastronomy": `8363`,
1370  Protella: `9830`,
1371  Protest: `550`,
1372  Proviz: `2188`,
1373  Prowheel: `4890`,
1374  PTN: `7761`,
1375  Pulifren: `7899`,
1376  Pulseroll: `8825`,
1377  Puma: `514`,
1378  Pure2improve: `8409`,
1379  Purepower: `9170`,
1380  Puressentiel: `3978`,
1381  Puro: `2921`,
1382  "Push Bars": `2880`,
1383  "Pyc Chain": `7470`,
1384  Pygomic: `7074`,
1385  "Q36.5": `3380`,
1386  Qardio: `2917`,
1387  Qbike: `7895`,
1388  QM: `1684`,
1389  Qplay: `7444`,
1390  "QU-AX": `3682`,
1391  Quarq: `2999`,
1392  Quaxar: `4901`,
1393  "Quick Media Electronic": `4359`,
1394  Quiksilver: `894`,
1395  Quoc: `8538`,
1396  "R-evenge": `647`,
1397  "r.s.p": `6739`,
1398  R2: `6410`,
1399  "Race Face": `3658`,
1400  "Race One": `2712`,
1401  Racer: `8350`,
1402  Racingbros: `7508`,
1403  Rad: `9166`,
1404  "Radio Bikes": `9303`,
1405  Raidlight: `2909`,
1406  Rainlegs: `4701`,
1407  Raleigh: `5000`,
1408  "Ram Mounts": `3391`,
1409  Ras: `2320`,
1410  Raskullz: `5867`,
1411  Rassine: `9578`,
1412  Ravemen: `10037`,
1413  "Raw Elements": `8933`,
1414  Rayovac: `6277`,
1415  "RDX Sports": `2697`,
1416  "Re-Fuel": `2931`,
1417  Realme: `4972`,
1418  "Recon Instrument": `10468`,
1419  Record: `4995`,
1420  "Red Bull Spect": `4985`,
1421  "Red Chili": `2424`,
1422  Reebok: `664`,
1423  Reelight: `4702`,
1424  "Reflectiv Spray": `7252`,
1425  Regatta: `2973`,
1426  "Rehab Medic": `5335`,
1427  Rehband: `3387`,
1428  Reich: `4715`,
1429  Reid: `2375`,
1430  Reisenthel: `4704`,
1431  Relber: `8605`,
1432  "Rendiment Race": `5451`,
1433  "Rene Furterer": `3968`,
1434  Renthal: `3675`,
1435  Repente: `6390`,
1436  Replay: `3656`,
1437  Reserve: `9388`,
1438  Resolvbike: `8910`,
1439  Respro: `5439`,
1440  Restrap: `8407`,
1441  Reusch: `477`,
1442  "Reverse Components": `3775`,
1443  Revoloop: `4845`,
1444  Rfr: `8076`,
1445  "Rfx Care": `8808`,
1446  "rh+": `1356`,
1447  Riday: `4499`,
1448  "Ride Concepts": `4935`,
1449  Ridefyl: `9327`,
1450  Ridewill: `4660`,
1451  Ridewrap: `7939`,
1452  Ridley: `6416`,
1453  Riff: `1118`,
1454  Rinat: `642`,
1455  "Rip Curl": `615`,
1456  Ritchey: `173`,
1457  "Rixen&Kaul": `4661`,
1458  Robens: `3429`,
1459  "Rock Experience": `5589`,
1460  "Rock Tape": `8273`,
1461  RockShox: `2035`,
1462  Roeckl: `859`,
1463  Rogelli: `7483`,
1464  Rohloff: `4662`,
1465  Roland: `4615`,
1466  Rondo: `4905`,
1467  Roodol: `9767`,
1468  Rosaura: `9888`,
1469  Rossignol: `259`,
1470  Roswheel: `7885`,
1471  Roto: `5168`,
1472  Rotor: `3172`,
1473  Rowenta: `5476`,
1474  Rox: `5440`,
1475  Roxy: `895`,
1476  Royalcycle: `7038`,
1477  RPM: `3897`,
1478  RRP: `7505`,
1479  RS7: `1624`,
1480  RST: `5472`,
1481  Rtech: `3639`,
1482  Rucanor: `1221`,
1483  "Rude Health": `1712`,
1484  "Rudy Project": `176`,
1485  Rukka: `318`,
1486  Runtastic: `598`,
1487  Ryder: `4663`,
1488  RymeBikes: `1944`,
1489  "S-ride": `7039`,
1490  Saccon: `3898`,
1491  "Safe Max": `2804`,
1492  "Safe Sea": `1511`,
1493  Saft: `8206`,
1494  Safta: `4314`,
1495  Sahmurai: `8533`,
1496  Sahoo: `7884`,
1497  Sailfish: `865`,
1498  Salewa: `178`,
1499  Salice: `2015`,
1500  "Salt&stone": `9497`,
1501  Salter: `460`,
1502  Saltstick: `1685`,
1503  "Salty Crew": `4534`,
1504  Salvimar: `869`,
1505  Samox: `5318`,
1506  Samsung: `2947`,
1507  Sanitas: `8348`,
1508  "Sanity Derm": `7891`,
1509  Sanon: `9823`,
1510  "Santa Cruz Bikes": `9301`,
1511  "Santa Madre": `8822`,
1512  Santini: `1230`,
1513  Sapience: `5951`,
1514  Sapim: `4922`,
1515  Sapo: `7800`,
1516  Saris: `3070`,
1517  Satori: `5322`,
1518  "SAXX Underwear": `2758`,
1519  Sbk: `9437`,
1520  SBS: `4360`,
1521  Schwalbe: `182`,
1522  Schwarz: `4617`,
1523  SCICON: `8711`,
1524  Scott: `1745`,
1525  "Scuba Gifts": `6412`,
1526  Scylla: `9013`,
1527  "SDG Components": `4833`,
1528  "SE Bikes": `5278`,
1529  "Sea To Summit": `392`,
1530  SEAC: `186`,
1531  Seachoice: `1425`,
1532  Sealskinz: `1735`,
1533  SeaSucker: `3636`,
1534  Seeland: `5749`,
1535  Seland: `8934`,
1536  Select: `8629`,
1537  "Selle Bassano": `3875`,
1538  "Selle Italia": `191`,
1539  "Selle Montegrappa": `7797`,
1540  "Selle Royal": `1477`,
1541  "Selle San Marco": `278`,
1542  "Selle San Remo": `9014`,
1543  "Selle SMP": `1942`,
1544  Sena: `2234`,
1545  Sencillo: `10466`,
1546  Senda: `7753`,
1547  Servivita: `9834`,
1548  Setlaz: `3899`,
1549  Seven: `3021`,
1550  "SH+": `2905`,
1551  Shapeheart: `6389`,
1552  Shimano: `193`,
1553  "Shock Absorber": `2444`,
1554  "Shock Doctor": `2876`,
1555  Shot: `2561`,
1556  "Shot Race Gear": `8657`,
1557  Shotgun: `4827`,
1558  Shun: `4806`,
1559  Sidas: `2201`,
1560  Sidi: `194`,
1561  "Sierra Climbing": `7833`,
1562  Sigalsub: `1461`,
1563  Sigeyi: `9575`,
1564  Sigma: `196`,
1565  Sikendiet: `9837`,
1566  Silca: `8219`,
1567  Silic1: `4807`,
1568  Silva: `197`,
1569  Silvano: `8752`,
1570  "Singing Rock": `1241`,
1571  Sinner: `3044`,
1572  Sinter: `8978`,
1573  Siren: `6947`,
1574  Siroko: `8696`,
1575  SIS: `1053`,
1576  Sisley: `4005`,
1577  "Six Union": `7040`,
1578  Sixpro: `9754`,
1579  Sixs: `2579`,
1580  Sixsixone: `7502`,
1581  Skateflash: `3689`,
1582  SKF: `4770`,
1583  Skins: `198`,
1584  SKS: `2054`,
1585  Slc: `8075`,
1586  Slime: `3825`,
1587  "Slug Plug": `9263`,
1588  Smart: `2716`,
1589  Smartek: `8747`,
1590  Smartgyro: `5663`,
1591  Smartshake: `2397`,
1592  Smartwool: `3042`,
1593  Smith: `201`,
1594  Smoove: `4782`,
1595  "Snap Climbing": `3420`,
1596  Sockguy: `7558`,
1597  Sockla: `4346`,
1598  Soehnle: `6195`,
1599  Softee: `4897`,
1600  Sogo: `8748`,
1601  Solaray: `7059`,
1602  Solgar: `6724`,
1603  Soll: `434`,
1604  Sony: `574`,
1605  Soudal: `3388`,
1606  Soundmoovz: `8689`,
1607  Source: `4182`,
1608  "SP Connect": `3829`,
1609  Spalding: `443`,
1610  Sparraw: `7712`,
1611  Spatzwear: `8796`,
1612  SPC: `2695`,
1613  "Specialites TA": `4866`,
1614  Specialized: `4929`,
1615  Speedo: `207`,
1616  Speedplay: `208`,
1617  Speedsleev: `4778`,
1618  Spektrum: `8508`,
1619  Spetton: `209`,
1620  Spich: `9292`,
1621  Spidi: `427`,
1622  Spinergy: `7879`,
1623  Spiro: `8797`,
1624  Spiuk: `433`,
1625  Spokey: `9401`,
1626  "Sport HG": `1234`,
1627  Sportful: `211`,
1628  "Sporti France": `8640`,
1629  Sportlast: `2545`,
1630  Sportourer: `279`,
1631  Sprayke: `7801`,
1632  SPY: `2030`,
1633  Spyder: `415`,
1634  Spypoint: `8439`,
1635  SQlab: `4990`,
1636  Squire: `2955`,
1637  "Squirt Cycling Products": `4880`,
1638  "SR Suntour": `3530`,
1639  Sram: `212`,
1640  "Stages Cycling": `3434`,
1641  Stance: `2971`,
1642  "Stans No Tubes": `4156`,
1643  "Star Blubike": `5963`,
1644  Starter: `6767`,
1645  Steadyrack: `4759`,
1646  Stealth: `8976`,
1647  Stique: `8611`,
1648  Stronglight: `3589`,
1649  Stucchi: `8857`,
1650  "Sun Ringle": `8975`,
1651  "Sun Zapper": `9504`,
1652  Sunny: `4842`,
1653  SunRace: `3048`,
1654  Sunstech: `2350`,
1655  Suomy: `1106`,
1656  Supacaz: `3694`,
1657  "Super B": `2978`,
1658  Superdry: `1577`,
1659  "Superior Bikes": `7745`,
1660  Superthings: `8882`,
1661  Sural: `2189`,
1662  "Surbikes Premium Socks": `4791`,
1663  Swede: `8380`,
1664  Swimovate: `216`,
1665  SwissStop: `1465`,
1666  Swisstone: `4579`,
1667  Syncros: `8884`,
1668  Synpowell: `4808`,
1669  Sytech: `7357`,
1670  "T-One": `2907`,
1671  Tactic: `4462`,
1672  Tacx: `429`,
1673  Tailwind: `9002`,
1674  Talamex: `5460`,
1675  "Tall Order": `8972`,
1676  Tange: `5375`,
1677  Tangent: `6692`,
1678  Tanita: `1484`,
1679  Tannus: `4636`,
1680  Target: `9015`,
1681  Tatonka: `3759`,
1682  Taurus: `5552`,
1683  Taya: `4883`,
1684  Taymory: `1678`,
1685  "Tech Light": `3134`,
1686  Technoline: `6040`,
1687  Technomousse: `8703`,
1688  Techwell: `7888`,
1689  Tecnomar: `221`,
1690  Tecnovita: `7275`,
1691  Tecxus: `7677`,
1692  Tektro: `3900`,
1693  Tenzing: `6987`,
1694  Ternua: `2194`,
1695  "Terry Fisio": `8906`,
1696  Texenergy: `7277`,
1697  Texlock: `7278`,
1698  "Tfa Dostmann": `6041`,
1699  TFHPC: `3907`,
1700  "The Beam": `8974`,
1701  "The Capsoul": `9825`,
1702  "The North Face": `224`,
1703  TheBeam: `3824`,
1704  TheraBand: `1692`,
1705  Theragun: `4976`,
1706  "Therm-ic": `225`,
1707  Thermowave: `9005`,
1708  Thinkrider: `7828`,
1709  Thirtytwo: `8521`,
1710  Thomson: `3336`,
1711  Thor: `3698`,
1712  Thuasne: `8648`,
1713  Thule: `538`,
1714  Tifosi: `480`,
1715  "Tiger Balm": `8663`,
1716  Tigra: `2933`,
1717  Tile: `3720`,
1718  Time: `499`,
1719  "Tip Top": `3896`,
1720  Titici: `8697`,
1721  Tkx: `8858`,
1722  Toad: `8512`,
1723  "Toei Animation": `5841`,
1724  Togs: `4881`,
1725  "Toimsa Bikes": `8881`,
1726  Tols: `5148`,
1727  Toorx: `7849`,
1728  Topaz: `9555`,
1729  Topeak: `3446`,
1730  Torch: `7285`,
1731  Torhans: `9757`,
1732  Torq: `4790`,
1733  Tortue: `8799`,
1734  "Total Bmx": `8928`,
1735  Totto: `6707`,
1736  "Totum Sport": `3435`,
1737  TouchCam: `2293`,
1738  "Tout Terrain": `3912`,
1739  Tovatec: `1086`,
1740  "Trail Gator": `4762`,
1741  Trangoworld: `228`,
1742  Transfil: `3883`,
1743  TranzX: `4769`,
1744  "Travel Blue": `1919`,
1745  Treefrog: `3787`,
1746  Trelock: `229`,
1747  Tremblay: `8644`,
1748  Trespass: `1544`,
1749  Triggerpoint: `7554`,
1750  Trigon: `579`,
1751  Trimm: `10356`,
1752  Trinca: `8556`,
1753  Trislide: `2339`,
1754  Tristar: `5554`,
1755  Triswim: `2340`,
1756  "Troy Lee Designs": `2016`,
1757  "True Utility": `1734`,
1758  Trumpf: `5376`,
1759  Truvativ: `231`,
1760  Tubolight: `4673`,
1761  Tubolito: `3798`,
1762  Tubus: `3361`,
1763  "Tucano Urbano": `8506`,
1764  Tufo: `2597`,
1765  Tunturi: `8707`,
1766  Turbo: `268`,
1767  Tusa: `232`,
1768  "Twenty One": `9670`,
1769  TwoNav: `233`,
1770  TYR: `507`,
1771  "Tyre Yogurt": `9302`,
1772  TyreKey: `4882`,
1773  Ubike: `9351`,
1774  Uco: `1920`,
1775  Uebler: `7409`,
1776  Ufesa: `5477`,
1777  UFO: `3190`,
1778  Ufor: `4784`,
1779  Uhlsport: `627`,
1780  Ulac: `7886`,
1781  ULD: `3872`,
1782  Uller: `4249`,
1783  "Ultimate Performance": `2212`,
1784  Ultrabreathe: `3821`,
1785  Ultraspire: `2468`,
1786  Umbro: `1721`,
1787  Umit: `8859`,
1788  Unex: `4996`,
1789  Union: `4705`,
1790  Unior: `4666`,
1791  Unix: `4763`,
1792  UNO: `4809`,
1793  UNPASS: `5433`,
1794  "Urban Classics": `8837`,
1795  "Urban Iki": `3585`,
1796  "Urban Motion": `8543`,
1797  "Urban Proof": `2915`,
1798  Urbanbiker: `7923`,
1799  Urge: `6048`,
1800  Ursus: `3874`,
1801  USE: `4785`,
1802  USWE: `3935`,
1803  Uvex: `542`,
1804  UYN: `5330`,
1805  "V8 Concepts": `8927`,
1806  Valvul: `9309`,
1807  "Van Allen": `6770`,
1808  VAR: `2209`,
1809  Varta: `5513`,
1810  VAUDE: `241`,
1811  Vavert: `9093`,
1812  VDO: `3827`,
1813  "VEE Rubber": `3884`,
1814  Velco: `5157`,
1815  Velo: `2757`,
1816  "Velo Orange": `8926`,
1817  Veloflex: `2598`,
1818  Velosock: `2376`,
1819  VeloToze: `4792`,
1820  Velox: `3885`,
1821  Venerate: `7811`,
1822  Venom: `8565`,
1823  Ventura: `1994`,
1824  Venzo: `2908`,
1825  Verbatim: `4443`,
1826  VHS: `7783`,
1827  Vicma: `3886`,
1828  "Victory Endurance": `4767`,
1829  Vidaxl: `8484`,
1830  Vincita: `8705`,
1831  Vinnic: `7294`,
1832  Viro: `7758`,
1833  Vision: `370`,
1834  "Vital Bike": `8562`,
1835  "Vital Gym": `8561`,
1836  Vittoria: `2042`,
1837  Vivasport: `7029`,
1838  VK: `3901`,
1839  Vnoise: `5434`,
1840  Volcom: `1534`,
1841  VP: `3887`,
1842  "Vp-components": `4894`,
1843  VR46: `3672`,
1844  Vredestein: `1015`,
1845  Vuelta: `9319`,
1846  Vueltausa: `7765`,
1847  Vulcanet: `5435`,
1848  Vulkan: `8999`,
1849  W2W: `1080`,
1850  Wag: `8084`,
1851  "Wag Bike Bicycle Components": `9764`,
1852  Wahoo: `1660`,
1853  Walio: `9574`,
1854  Wanda: `7041`,
1855  Wantalis: `7296`,
1856  "Warner Bros": `5850`,
1857  Wasp: `3562`,
1858  Watteam: `3706`,
1859  "WD-40": `2343`,
1860  Wd40: `8225`,
1861  Weefine: `2247`,
1862  WeeRide: `7107`,
1863  Weider: `1304`,
1864  Weldtite: `2717`,
1865  Wellgo: `4992`,
1866  Wens: `7903`,
1867  Wera: `6219`,
1868  Weslo: `8860`,
1869  Wheels: `7509`,
1870  "Wheels Manufacturing": `4788`,
1871  "White Lightning": `5715`,
1872  Widek: `4708`,
1873  Wiener: `4941`,
1874  "Wild Republic": `5446`,
1875  Wildcountry: `876`,
1876  Willworx: `4942`,
1877  Wilson: `245`,
1878  "Win It": `9562`,
1879  "Wind X-Treme": `1290`,
1880  Winora: `4638`,
1881  Wippermann: `3590`,
1882  Withings: `4945`,
1883  Woho: `3096`,
1884  Wolfpack: `4781`,
1885  Woomax: `5851`,
1886  Wowow: `4709`,
1887  Woxter: `5661`,
1888  WRC: `8083`,
1889  WTB: `562`,
1890  Wueps: `8482`,
1891  "X-adventurer": `3641`,
1892  "X-BIONIC": `270`,
1893  "X-Sauce": `2884`,
1894  "X-SOCKS": `247`,
1895  "Xavier Mor": `8457`,
1896  Xcel: `927`,
1897  Xena: `9074`,
1898  Xerama: `4895`,
1899  Xiaomi: `2696`,
1900  XLAB: `1731`,
1901  XLC: `3500`,
1902  Xpedo: `3663`,
1903  Xplova: `3186`,
1904  "Xq Max": `8430`,
1905  Yaban: `4810`,
1906  Yamaha: `5159`,
1907  Ybn: `9320`,
1908  Yeah: `8861`,
1909  Yessbmx: `8798`,
1910  Yeti: `5426`,
1911  Yoko: `8641`,
1912  Yokozuna: `6497`,
1913  Youin: `7860`,
1914  YST: `7740`,
1915  Zamst: `570`,
1916  "Zan Headgear": `4279`,
1917  Zanchetta: `7802`,
1918  Zasdar: `8704`,
1919  Zefal: `626`,
1920  Zeray: `7896`,
1921  Zerod: `366`,
1922  ZeroFlats: `1583`,
1923  Ziener: `3029`,
1924  Zipp: `250`,
1925  Zoggs: `380`,
1926  Zone3: `1062`,
1927  Zoom: `374`,
1928  Zoot: `251`,
1929  ZVG: `4711`,
1930  Zycle: `4727`,
1931};
1932
1933function slugize(str) {
1934  // taken directly from tradeinn js
1935  str = str.replace(/à/gi, `a`);
1936  str = str.replace(/á/gi, `a`);
1937  str = str.replace(/â/gi, `a`);
1938  str = str.replace(/ã/gi, `a`);
1939  str = str.replace(/ä/gi, `a`);
1940  str = str.replace(/å/gi, `a`);
1941  str = str.replace(/ä/gi, `a`);
1942  str = str.replace(/à/gi, `a`);
1943  str = str.replace(/á/gi, `a`);
1944  str = str.replace(/â/gi, `a`);
1945  str = str.replace(/ã/gi, `a`);
1946  str = str.replace(/ä/gi, `a`);
1947  str = str.replace(/å/gi, `a`);
1948  str = str.replace(/ä/gi, `a`);
1949  str = str.replace(/ò/gi, `o`);
1950  str = str.replace(/ó/gi, `o`);
1951  str = str.replace(/ô/gi, `o`);
1952  str = str.replace(/õ/gi, `o`);
1953  str = str.replace(/ö/gi, `o`);
1954  str = str.replace(/ø/gi, `o`);
1955  str = str.replace(/ò/gi, `o`);
1956  str = str.replace(/ó/gi, `o`);
1957  str = str.replace(/ô/gi, `o`);
1958  str = str.replace(/õ/gi, `o`);
1959  str = str.replace(/ö/gi, `o`);
1960  str = str.replace(/ø/gi, `o`);
1961  str = str.replace(//gi, `e`);
1962  str = str.replace(/è/gi, `e`);
1963  str = str.replace(/é/gi, `e`);
1964  str = str.replace(/ê/gi, `e`);
1965  str = str.replace(/ë/gi, `e`);
1966  str = str.replace(/è/gi, `e`);
1967  str = str.replace(/é/gi, `e`);
1968  str = str.replace(/ê/gi, `e`);
1969  str = str.replace(/ë/gi, `e`);
1970  str = str.replace(/ç/gi, `c`);
1971  str = str.replace(/ç/gi, `c`);
1972  str = str.replace(/ì/gi, `i`);
1973  str = str.replace(/í/gi, `i`);
1974  str = str.replace(/î/gi, `i`);
1975  str = str.replace(/ï/gi, `i`);
1976  str = str.replace(/ì/gi, `i`);
1977  str = str.replace(/í/gi, `i`);
1978  str = str.replace(/î/gi, `i`);
1979  str = str.replace(/ï/gi, `i`);
1980  str = str.replace(/ù/gi, `u`);
1981  str = str.replace(/ú/gi, `u`);
1982  str = str.replace(/û/gi, `u`);
1983  str = str.replace(/ü/gi, `u`);
1984  str = str.replace(/ù/gi, `u`);
1985  str = str.replace(/ú/gi, `u`);
1986  str = str.replace(/û/gi, `u`);
1987  str = str.replace(/ü/gi, `u`);
1988  str = str.replace(/ÿ/gi, `y`);
1989  str = str.replace(/ñ/gi, `n`);
1990  str = str.replace(/ñ/gi, `n`);
1991  str = str.replace(/\//gi, `-`);
1992  str = str.replace(/´/gi, ``);
1993  str = str.replace(/`/gi, `-`);
1994  str = str.replace(/&/gi, `-`);
1995  str = str.replace(/¬/gi, `-`);
1996  str = str.replace(/º/gi, `-`);
1997  str = str.replace(/!/gi, `-`);
1998  str = str.replace(/¡/gi, `-`);
1999  str = str.replace(/¿/gi, `-`);
2000  str = str.replace(/\?/gi, `-`);
2001  str = str.replace(/\+/gi, `-`);
2002  str = str.replace(/\(/gi, `-`);
2003  str = str.replace(/\)/gi, `-`);
2004  str = str.replace(/·/gi, `-`);
2005  str = str.replace(//gi, `-`);
2006  str = str.replace(/ª/gi, `-`);
2007  str = str.replace(//gi, `-`);
2008  str = str.replace(/®/gi, `-`);
2009  str = str.replace(/"/gi, `-`);
2010  str = str.replace(/°/gi, `-`);
2011  str = str.replace(/µ/gi, `u`);
2012  str = str.replace(/«/gi, `-`);
2013  str = str.replace(/»/gi, `-`);
2014  str = str.replace(/±/gi, `-`);
2015  str = str.replace(/²/gi, `-`);
2016  str = str.replace(/>/gi, `-`);
2017  str = str.replace(/</gi, `-`);
2018  str = str.replace(/ä/gi, `a`);
2019  str = str.replace(/ë/gi, `e`);
2020  str = str.replace(/ï/gi, `i`);
2021  str = str.replace(/ö/gi, `o`);
2022  str = str.replace(/ü/gi, `u`);
2023  str = str.replace(/¨/gi, `-`);
2024  str = str.replace(/ª/gi, `-`);
2025  str = str.replace(/½/gi, `-`);
2026  str = str.replace(/©/gi, `-`);
2027  str = str.replace(`/™/gi`, ``);
2028  str = str.replace(//gi, `-`);
2029  str = str.replace(//gi, `-`);
2030  str = str.replace(/æ/gi, `-`);
2031  str = str.replace(/©/gi, `-`);
2032  str = str.replace(/³/gi, `-`);
2033  str = str.replace(//gi, `-`);
2034  str = str.replace(/¤/gi, `-`);
2035  str = str.replace(/¦/gi, `-`);
2036  str = str.replace(/æ/gi, `-`);
2037  str = str.replace(/¥/gi, `-`);
2038  str = str.replace(/ÿ/gi, `y`);
2039  str = str.replace(/§/gi, `-`);
2040  str = str.replace(/œ/gi, `-`);
2041  str = str.replace(/ß/gi, `-`);
2042  str = str.replace(/œ/gi, `-`);
2043  str = str.replace(/ð/gi, `-`);
2044  str = str.replace(/¹/gi, `-`);
2045  str = str.replace(/ý/gi, `y`);
2046  str = str.replace(/¼/gi, `-`);
2047  str = str.replace(/¾/gi, `-`);
2048  str = str.replace(/£/gi, `-`);
2049  str = str.replace(/þ/gi, `-`);
2050  str = str.replace(/ /gi, `-`);
2051  str = str.replace(/%/gi, ``);
2052  str = str.replace(//gi, ``); // eslint-disable-line no-control-regex
2053  return str.toLowerCase();
2054}
2055
2056function makeImageUrl(id, title) {
2057  // @ts-ignore
2058  const bucket = parseInt(id / 10000);
2059  return [
2060    BASE_URL,
2061    `/m/`,
2062    bucket.toString(),
2063    `/`,
2064    id,
2065    `/`, // TODO: Higher res
2066    slugize(title),
2067    `.jpg`,
2068  ].join(``);
2069}
2070
2071function makeApiBody(params) {
2072  return params
2073    .map((x) =>
2074      x
2075        .toString() // can be number
2076        // TODO: Replace with proper encoding, but beware that we can't encode all characters!
2077        .replace(/;/g, `%3B`)
2078        .replace(/:/g, `%3A`)
2079        .replace(/,/g, `%2C`)
2080        .replace(/&/g, `%26`)
2081        .replace(/\[]/g, `%5B%5D`)
2082        .replace(/=/g, `%3D`)
2083        .replace(/@/g, `%40`)
2084    )
2085    .map((x) => encodeURI(`vars[]=`) + x)
2086    .concat([`texto_search=`])
2087    .join(`&`);
2088}
2089
2090async function makeApiCall({
2091  shopId = SHOPS.BIKEINN,
2092  brandId, // 212: SRAM, 159: POC, ...
2093  perPage = PER_PAGE, // TODO: Make it work with different per pages
2094  cursor = 0,
2095}) {
2096  const body = makeApiBody([
2097    `id_tienda=${shopId}@fecha_descatalogado=[now/d-*]`,
2098    `atributos_e=5091,6017`, // FIXME: Explain
2099    [
2100      // Lot of following probably not needed // TODO: Replace when introducing test suite
2101      `model.eng`,
2102      `id_marca`,
2103      `sostenible`,
2104      `productes.talla2`,
2105      `productes.talla_usa`,
2106      `productes.talla_jp`,
2107      `productes.talla_uk`,
2108      `tres_sesenta`,
2109      `atributos_padre.atributos.id_atribut_valor`,
2110      `productes.v360`,
2111      `productes.v180`,
2112      `productes.v90`,
2113      `productes.v30`,
2114      `productes.exist`,
2115      `productes.stock_reservat`,
2116      `productes.pmp`,
2117      `productes.id_producte`,
2118      `productes.color`,
2119      `productes.referencia`,
2120      `productes.brut`,
2121      `productes.desc_brand`,
2122      `image_created`,
2123      `id_modelo`,
2124      `familias.eng`,
2125      `familias.eng`,
2126      `familias.id_familia`,
2127      `familias.subfamilias.eng`,
2128      `familias.subfamilias.eng`,
2129      `familias.subfamilias.id_tienda`,
2130      `familias.subfamilias.id_subfamilia`,
2131      `productes.talla`,
2132      `productes.baja`,
2133      `productes.rec`,
2134      `precio_win_54`,
2135      `productes.sellers.id_seller`,
2136      `productes.sellers.precios_paises.precio`,
2137      `productes.sellers.precios_paises.id_pais`,
2138      `fecha_descatalogado`,
2139      `marca`,
2140    ].join(`;`),
2141    `precio_win_54;asc`,
2142    perPage,
2143    `productos`,
2144    `search`,
2145    `marca=${brandId}`,
2146    cursor,
2147  ]);
2148
2149  const resRaw = await fetch(
2150    `https://www.tradeinn.com/index.php` +
2151      `?action=get_info_elastic_listado` +
2152      `&id_tienda=${shopId}` + // TODO: Selectable
2153      `&idioma=eng`,
2154    {
2155      method: `POST`,
2156      headers: {
2157        // From my testing, the following header are required
2158        Accept: `application/json, text/javascript, */*; q=0.01`,
2159        "Accept-Language": `en-US,en`,
2160        "Content-Type": `application/x-www-form-urlencoded; charset=UTF-8`,
2161        Cookie: `id_pais=${COUNTRIES.Germany};`, // required to get `item.precio_win`
2162      },
2163      body,
2164    }
2165  );
2166
2167  const resJson = await resRaw.json();
2168  return resJson;
2169}
2170
2171async function enqueueInitial(mode, crawler, shop) {
2172  if (mode === MODE.FULL) {
2173    await crawler.addRequests([
2174      {
2175        userData: { label: LABEL.INDEX },
2176        uniqueKey: LABEL.INDEX,
2177        url: `https://dummy.com`,
2178      },
2179    ]);
2180  } else if (mode === MODE.TEST) {
2181    const query = {
2182      shopId: SHOPS[shop],
2183      brandId: BRANDS.POC,
2184    };
2185    await crawler.addRequests([
2186      {
2187        userData: { label: LABEL.PRODUCTS, query },
2188        uniqueKey: JSON.stringify(query),
2189        url: `https://dummy.com`,
2190      },
2191    ]);
2192  } else if (mode === MODE.CURATED) {
2193    const brands = [
2194      BRANDS.POC,
2195      BRANDS.Sram,
2196      BRANDS[`3M`],
2197      BRANDS.Assos,
2198      BRANDS.BBB,
2199      BRANDS[`Bike Workx`],
2200      BRANDS[`Bike Yoke`],
2201      BRANDS[`Black Diamond`],
2202      BRANDS.Blackburn,
2203      BRANDS.Bombtrack,
2204      BRANDS.Camelbak,
2205      BRANDS.Chrome,
2206      BRANDS.Crankbrothers,
2207      BRANDS.Dakine,
2208      BRANDS.Douchebags,
2209      BRANDS[`DT Swiss`],
2210      BRANDS.Dynafit,
2211      BRANDS[`E-thirteen`],
2212      BRANDS.Ergon,
2213      BRANDS.Evoc,
2214      BRANDS[`F-Lite`],
2215      BRANDS.Fidlock,
2216      BRANDS[`Finish Line`],
2217      BRANDS[`Five Ten`],
2218      BRANDS.Fizik,
2219      BRANDS.Flashmer,
2220      BRANDS.Fox,
2221      BRANDS.Fuji,
2222      BRANDS.Fulcrum,
2223      BRANDS.FullGas,
2224      BRANDS[`G-Star`],
2225      BRANDS.Giro,
2226      BRANDS.GoPro,
2227      BRANDS.Graff,
2228      BRANDS.Hope,
2229      BRANDS.HT,
2230      BRANDS.Icebreaker,
2231      BRANDS.iXS,
2232      BRANDS.Kask,
2233      BRANDS.Leatt,
2234      BRANDS.Lezyne,
2235      BRANDS[`Lizard Skins`],
2236      BRANDS.Magura,
2237      BRANDS.Maxxis,
2238      BRANDS.MET,
2239      BRANDS.Michelin,
2240      BRANDS[`Muc Off`],
2241      BRANDS.Nike,
2242      BRANDS.Niner,
2243      BRANDS.Oakley,
2244      BRANDS.Ortlieb,
2245      BRANDS.Osprey,
2246      BRANDS[`O´neill`],
2247      BRANDS[`Park Tool`],
2248      BRANDS.POC,
2249      BRANDS[`Race Face`],
2250      BRANDS.Ritchey,
2251      BRANDS.RockShox,
2252      BRANDS.Schwalbe,
2253      BRANDS.Scott,
2254      BRANDS[`Sea To Summit`],
2255      BRANDS[`Shock Doctor`],
2256      BRANDS.Smith,
2257      BRANDS.Specialized,
2258      BRANDS.SQlab,
2259      BRANDS[`SR Suntour`],
2260      BRANDS.Sram,
2261      BRANDS.SunRace,
2262      BRANDS[`Super B`],
2263      BRANDS[`Superior Bikes`],
2264      BRANDS[`The North Face`],
2265      BRANDS.Thule,
2266      BRANDS.Topeak,
2267      BRANDS.TranzX,
2268      BRANDS[`Troy Lee Designs`],
2269      BRANDS.Tubolito,
2270      BRANDS.Tufo,
2271      BRANDS.Vittoria,
2272      BRANDS.Wahoo,
2273      BRANDS.Wolfpack,
2274      BRANDS.WTB,
2275      BRANDS.Xpedo,
2276      BRANDS.Zefal,
2277      BRANDS.Zipp,
2278    ];
2279    for (const brandId of brands) {
2280      const query = { shopId: SHOPS[shop], brandId };
2281      await crawler.addRequests([
2282        {
2283          userData: { label: LABEL.PRODUCTS, query },
2284          uniqueKey: JSON.stringify(query),
2285          url: `https://dummy.com`,
2286        },
2287      ]);
2288    }
2289  }
2290}
2291
2292const router = createBasicRouter();
2293
2294router.addHandler(LABEL.INDEX, async ({ request, $ }) => {
2295  // FIXME
2296});
2297
2298router.addHandler(LABEL.PRODUCTS, async ({ request, crawler }) => {
2299  console.log(`[handleCategory]`, JSON.stringify(request.userData));
2300
2301  const res = await makeApiCall(request.userData.query);
2302
2303  const total = res.total.value;
2304  console.log(`[handleCategory] total`, total);
2305  if (!request.userData.query.cursor) {
2306    // not present or 0
2307    for (let i = 0; i <= total; i += PER_PAGE) {
2308      // skip first page, that is already handled
2309      const query = { ...request.userData.query, cursor: i };
2310      console.log(`[handleCategory] enqueuing pages`, JSON.stringify(query)); // FIXME
2311      void crawler.addRequests([
2312        {
2313          userData: { label: LABEL.PRODUCTS, query },
2314          uniqueKey: JSON.stringify(query),
2315          url: `https://dummy.com`,
2316        },
2317      ]);
2318    }
2319  }
2320
2321  const items = res.id_modelos;
2322
2323  const products = [];
2324  for (const item of items) {
2325    // ID
2326    const pid = item.id_modelo;
2327
2328    // Title & URL
2329    const title =
2330      item.marca + ` ` + (item.nombre_modelo_eng ?? item.nombre_modelo);
2331    const url = `https://www.tradeinn.com/bikeinn/en/${slugize(
2332      title
2333    )}/${pid}/p`;
2334
2335    // Prices
2336    let currentPrice = item.precio_win;
2337    if (!currentPrice) {
2338      for (const product of item.productes) {
2339        const discontinued = product.baja; // 0 = available, 1 = discontinued
2340        if (discontinued !== `0`) continue;
2341        if (!product.sellers?.length) continue;
2342        if (!product.sellers[0]) continue;
2343        let seller = product.sellers.find((x) => x.id_seller === 1); // Seller #1 has preffered price, taken from TradeInn FE code
2344        if (!seller) seller = product.sellers[0]; // TODO: Maybe find one with best price?
2345        currentPrice = seller.precio_producte;
2346        if (currentPrice) break;
2347      }
2348    }
2349    if (!currentPrice) {
2350      console.log(`[handleCategory] currentPrice not found`);
2351      debugger;
2352    }
2353
2354    const originalPrice = parseFloat(item.productes?.[0]?.rec);
2355    if (!originalPrice) {
2356      console.log(`[handleCategory] originalPrice not found`);
2357    }
2358
2359    // // Inbox
2360    // const products = item["productes"] ?? item["_source"]["productes"];
2361    // // TODO: Explain
2362    // const categories = item["familias"] ?? item["_source"]["familias"];
2363    // data[i_mc]["_source"]["precio_win_"+info_pais["id_pais"]];
2364    // data[i_mc]["_source"]["model"]["eng"];
2365
2366    // Other
2367    const _category = item.familias
2368      .map((x) => `${x.eng} > ${x.subfamilias.eng}`)
2369      .join(` | `);
2370    const img = makeImageUrl(pid, title);
2371
2372    // Derived
2373    const discount = originalPrice
2374      ? Math.round((1 - currentPrice / originalPrice) * 100)
2375      : undefined;
2376
2377    const discountAbs = originalPrice
2378      ? originalPrice - currentPrice
2379      : undefined;
2380
2381    const product = {
2382      pid,
2383      url,
2384      name: title,
2385      img,
2386      currentPrice,
2387      originalPrice, // string
2388      currency: `EUR`,
2389
2390      // Derived
2391      _discount: discount,
2392    };
2393
2394    // console.log("⬇⬇⬇")
2395    // console.log(JSON.stringify(product, null, 2))
2396    // console.log("⬆⬆⬆")
2397    products.push(product);
2398  }
2399  await save(products);
2400});
2401
2402void Actor.main(async () => {
2403  const input = await Actor.getInput();
2404  const {
2405    debug = false,
2406    mode = MODE.CURATED, // FIXME: Full
2407    shop = SHOP.BIKEINN,
2408    ...rest
2409  } = input ?? {};
2410  await init({ actorNameOverride: `tradeinn-com` }, rest);
2411  const crawler = new BasicCrawler({
2412    useSessionPool: true,
2413    requestHandler: router,
2414  });
2415  await enqueueInitial(mode, crawler, shop);
2416  await crawler.run();
2417});

package.json

1{
2  "name": "tradeinn-tradeinn-com-scraper",
3  "description": "Scrapes products titles, prices, images and availability. Does NOT scrape product details.",
4  "type": "module",
5  "scripts": {
6    "start": "node ./main.js",
7    "push-to-apify-platform": "npx apify push"
8  },
9  "dependencies": {
10    "apify3": "npm:apify@^3.0.2",
11    "crawlee": "*",
12    "node-fetch": "*",
13    "pg": "*",
14    "pg-connection-string": "*",
15    "dotenv": "*",
16    "find-config": "*",
17    "@elastic/elasticsearch": "*",
18    "filenamify": "*"
19  },
20  "apify": {
21    "title": "Tradeinn (tradeinn.com) scraper",
22    "description": "Scrapes products titles, prices, images and availability. Does NOT scrape product details.",
23    "isPublic": true,
24    "isDeprecated": false,
25    "isAnonymouslyRunnable": true,
26    "notice": "",
27    "pictureUrl": "",
28    "seoTitle": "",
29    "seoDescription": "",
30    "categories": [
31      "ECOMMERCE"
32    ]
33  }
34}

.actor/logo.png

_utils/common.js

1import { createHash } from 'crypto'
2import os from "os"
3import path from "path"
4// eslint-disable-next-line @apify/apify-actor/no-forbidden-node-internals
5import fs from "fs"
6import pg from "pg"
7import pgConnectionString from 'pg-connection-string'
8import { config } from 'dotenv'
9import findConfig from "find-config"
10import { Client as ElasticClient } from "@elastic/elasticsearch"
11import filenamify from 'filenamify'
12import { Dataset } from 'crawlee'
13
14config({ path: findConfig(`.env`) })
15
16const elasticIndexName = `actors-monorepo-shops`
17
18const globalLogsProps = {
19  __NODE_STARTED: new Date().toISOString(),
20}
21
22let actorName
23let pgClient
24let pgClientNormalized
25let elasticClient
26export async function init ({ actorNameOverride }, restInput) {
27  parseEnvFromInput(restInput)
28
29  if (os.platform() === `darwin`) {
30    const filePath = process.argv[1] // ~/Projects/apify-actors-monorepo/actors/foo.ts
31    const basename = path.basename(filePath) // foo.ts
32    actorName = actorNameOverride ?? basename.split(`.`)[0] // foo
33    const gitBranch = fs.readFileSync(path.join(process.cwd(), `..`, `.git/HEAD`), `utf8`)
34      .split(` `)[1]
35      .trim()
36      .replace(`refs/heads/`, ``)
37    const gitCommit = fs.readFileSync(path.join(process.cwd(), `..`, `.git/refs/heads/${gitBranch}`), `utf8`)
38    const gitCommitShort = gitCommit.substring(0, 7)
39    globalLogsProps.__GIT_COMMIT = gitCommitShort
40  }
41
42  if (process.env.APIFY_IS_AT_HOME) {
43    actorName = actorNameOverride ?? process.env.APIFY_ACTOR_ID // Name would be better, but it's not in ENV
44  }
45
46  /* ELASTIC */
47  /* ======= */
48  if (process.env.ELASTIC_CLOUD_ID) {
49    elasticClient = new ElasticClient({
50      cloud: { id: process.env.ELASTIC_CLOUD_ID },
51      auth: { apiKey: process.env.ELASTIC_CLOUD_API_KEY },
52    })
53
54    // const mapping = await elasticClient.indices.getMapping({ index: actorName })
55
56    // eslint-disable-next-line no-inner-declarations
57    async function enforceIndexMapping () {
58      const doesIndexExist = await elasticClient.indices.exists({ index: elasticIndexName })
59      if (!doesIndexExist) await elasticClient.indices.create({ index: elasticIndexName })
60      await elasticClient.indices.putMapping({
61        index: elasticIndexName,
62        body: {
63          properties: {
64            _discount: { type: `float` },
65            originalPrice: { type: `float` },
66            currentPrice: { type: `float` },
67          },
68        },
69      })
70    }
71
72    try {
73      await enforceIndexMapping()
74    } catch (err) {
75      if (err.message.includes(`cannot be changed from type`)) {
76        console.log(`Elastic index ${elasticIndexName} already exists with incorrect mappings. As existing mapping cannot be changed, index will be deleted and recreated.`)
77        await elasticClient.indices.delete({ index: elasticIndexName })
78        await enforceIndexMapping()
79      }
80    }
81  }
82
83  /* POSTGRESQL */
84  /* ========== */
85  if (process.env.PG_CONNECTION_STRING) {
86    const pgConfig = pgConnectionString(process.env.PG_CONNECTION_STRING)
87    // const pgPool = new pg.Pool(pgConfig)
88
89    pgClient = new pg.Client(pgConfig)
90    await pgClient.connect()
91
92    // Check if table exists and have proper columns
93    const { rows: tables } = await pgClient.query(`
94    SELECT table_name
95    FROM information_schema.tables
96    WHERE table_schema = 'public'
97  `)
98
99    // eslint-disable-next-line camelcase
100    const tableExists = tables.some(({ table_name }) => table_name === process.env.PG_DATA_TABLE)
101    if (!tableExists) {
102      throw new Error(`Table ${process.env.PG_DATA_TABLE} does not exist in database ${pgConfig.database}`)
103    }
104
105  // TODO: Handle pgClient closing
106  }
107
108  if (process.env.PG_CONNECTION_STRING_NORMALIZED) {
109    const pgConfig = pgConnectionString(process.env.PG_CONNECTION_STRING_NORMALIZED)
110
111    pgClientNormalized = new pg.Client(pgConfig)
112    await pgClientNormalized.connect()
113
114    // Check if table exists and have proper columns
115    const { rows: tables } = await pgClientNormalized.query(`
116    SELECT table_name
117    FROM information_schema.tables
118    WHERE table_schema = 'public'
119  `)
120
121    // eslint-disable-next-line camelcase
122    const tableMainExists = tables.some(({ table_name }) => table_name === process.env.PG_DATA_TABLE)
123    // eslint-disable-next-line camelcase
124    const tablePricesExists = tables.some(({ table_name }) => table_name === process.env.PG_DATA_PRICE_TABLE)
125    if (!tableMainExists) throw new Error(`Table ${process.env.PG_DATA_TABLE} does not exist in database ${pgConfig.database}`)
126    if (!tablePricesExists) throw new Error(`Table ${process.env.PG_DATA_PRICE_TABLE} does not exist in database ${pgConfig.database}`)
127
128  // TODO: Handle pgClient closing
129  }
130}
131
132// inspired by @drobnikj
133// TODO: Similar, but less obfuscated for easier debugging
134export const createUniqueKeyFromUrl = (url) => {
135  const hash = createHash(`sha256`)
136  const cleanUrl = url.split(`://`)[1] // Remove protocol
137  hash.update(cleanUrl)
138  return hash.digest(`hex`)
139}
140
141/**
142 *
143 * @param {Date} datetime
144 * @return {Promise<void>}
145 */
146export const sleepUntil = async (datetime) => {
147  const now = new Date()
148  const difference = datetime - now
149  if (difference > 0) {
150    return new Promise((resolve) => {
151      setTimeout(resolve, difference)
152    })
153  }
154  return Promise.resolve()
155}
156
157export function parsePrice (string) {
158  let amount, currency
159  const noText = string.replace(/[^\d,.]/g, ``)
160  const decimals = noText.match(/([,.])(\d{2})$/)
161  if (decimals) {
162    const decimalSeparator = decimals[1]
163    // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
164    const decimalAmount = decimals[2]
165    amount = parseInt(noText.split(decimalSeparator)[0])
166  } {
167    const justNumbers = noText.replace(/[,.]/g, ``)
168    amount = parseInt(justNumbers)
169  }
170  return { amount, currency }
171}
172
173export function toNumberOrNull (str) {
174  // TODO: Handle better, but only after adding test
175  if (str === undefined) return null
176  if (str === null) return null
177  if (str === ``) return null
178  const num = Number(str)
179  if (Number.isNaN(num)) return null
180  return num
181}
182
183export async function save (objs) {
184  if (!Array.isArray(objs)) objs = [objs]
185  if (objs.length === 0) return
186
187  const objsExtended = objs.map((obj) => {
188    const objExtended = {
189      ...obj,
190      actorName,
191      ...globalLogsProps,
192      // __NODE_VERSION: global.process.versions.node,
193      // __NODE_UPTIME: global.process.uptime().toFixed(2), // seconds, 2 decimals
194    }
195    // if run on Apify
196    if (process.env.APIFY_IS_AT_HOME) {
197      objExtended.__APIFY_ACTOR_ID = process.env.APIFY_ACTOR_ID
198      objExtended.__APIFY_ACTOR_RUN_ID = process.env.APIFY_ACTOR_RUN_ID
199      objExtended.__APIFY_ACTOR_BUILD_ID = process.env.APIFY_ACTOR_BUILD_ID
200      objExtended.__APIFY_ACTOR_BUILD_NUMBER = process.env.APIFY_ACTOR_BUILD_NUMBER
201      objExtended.__APIFY_ACTOR_TASK_ID = process.env.APIFY_ACTOR_TASK_ID
202      if (!process.env.APIFY_DONT_STORE_IN_DATASET) void Dataset.pushData(obj)
203    }
204    return objExtended
205  })
206  // if runs on local machine (MacOS)
207  if (os.platform() === `darwin`) {
208    const cwd = process.cwd() // ~/Projects/apify-actors-monorepo/actors
209    const storageDir = path.join(cwd, `${actorName}.storage`) // ~/Projects/apify-actors-monorepo/actors/foo.storage
210    if (!fs.existsSync(storageDir)) fs.mkdirSync(storageDir)
211    const dataDir = path.join(storageDir, `data`) // ~/Projects/apify-actors-monorepo/actors/foo.storage/data
212    if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir)
213    for (const objExtended of objsExtended) {
214      const id = objExtended.id ?? objExtended.pid // ?? uuidv4()
215      const fileName = `${filenamify(id)}.json`
216      const dataFilePath = path.join(dataDir, fileName) // ~/Projects/apify-actors-monorepo/actors/foo.storage/data/foo.json
217      fs.writeFileSync(dataFilePath, JSON.stringify(objExtended, null, 2))
218    }
219  }
220
221  if (pgClient) {
222    const objsPg = objs.map((obj) => ({
223      ...obj,
224      // TODO: This is becoming not nice, and not clear
225      shop: actorName,
226      scrapedAt: new Date().toISOString().split(`T`)[0],
227    }))
228
229    const columns = getColumns(objsPg)
230    const values = getValues(objsPg)
231    const queryString = `
232        INSERT INTO public."${process.env.PG_DATA_TABLE}" (${columns})
233        VALUES (${values})
234    `
235    try {
236      const { rowCount } = await pgClient.query(queryString)
237      console.log(`[save] saved to database: ${JSON.stringify(rowCount)}`)
238    } catch (err) {
239      if (err.message.includes(`violates unique constraint`)) console.warn(`PostgresSQL: violates unique constraint`)
240      else throw err
241    }
242  }
243
244  // Only make sense for HlidacShopu
245  if (pgClientNormalized) {
246    const objsPgData = objs.map((obj) => ({
247      shop: actorName,
248      pid: obj.pid,
249      name: obj.name,
250      url: obj.url,
251      img: obj.img,
252    }))
253
254    const objsPgDataPrice = objs.map((obj) => ({
255      shop: actorName,
256      pid: obj.pid,
257      scrapedAt: new Date().toISOString().split(`T`)[0],
258      currentPrice: obj.currentPrice,
259      originalPrice: obj.originalPrice,
260      inStock: obj.inStock,
261    }))
262
263    const queryString = `
264        INSERT INTO public."${process.env.PG_DATA_TABLE}" (${getColumns(objsPgData)})
265        VALUES (${getValues(objsPgData)})
266        ON CONFLICT DO NOTHING
267    `
268    try {
269      const { rowCount } = await pgClientNormalized.query(queryString)
270      console.log(`[save] saved to database (data): ${JSON.stringify(rowCount)}`)
271    } catch (err) {
272      if (err.message.includes(`violates unique constraint`)) console.warn(`PostgresSQL: violates unique constraint`)
273      else throw err
274    }
275
276    const queryStringPrice = `
277        INSERT INTO public."${process.env.PG_DATA_PRICE_TABLE}" (${getColumns(objsPgDataPrice)})
278        VALUES (${getValues(objsPgDataPrice)})
279        ON CONFLICT DO NOTHING
280    `
281    try {
282      const { rowCount } = await pgClientNormalized.query(queryStringPrice)
283      console.log(`[save] saved to database (price): ${JSON.stringify(rowCount)}`)
284    } catch (err) {
285      if (err.message.includes(`violates unique constraint`)) console.warn(`PostgresSQL: violates unique constraint`)
286      else throw err
287    }
288  }
289
290  if (elasticClient) {
291    // .index creates or updates the document
292    // .create creates a new document if it doesn't exist, 409 if it does
293    // try {
294    //   const res = await elasticClient.index({
295    //     index: `actors-monorepo-shops`, // TODO: Consider using actorName
296    //     id, // foo-bar
297    //     document: objExtended, // {...}
298    //   })
299    // } catch (err) {
300    //   // https://discuss.elastic.co/t/elasticsearch-503-ok-false-message-the-requested-deployment-is-currently-unavailable/200583
301    //   if (err.message.includes(`requested resource is currently unavailable`)) console.log(`Elasticsearch is unavailable, skipping, but not aborting`)
302    //   else throw err
303    // }
304  }
305}
306
307function getColumns (objs) {
308  return Object.keys(objs[0]).map((key) => `"${key}"`).join(`, `)
309}
310
311function getValues (objs) {
312  return objs.map(objPg => Object.values(objPg).map((value) => {
313    // escape strings to prevent SQL injection
314    if (typeof value === `string`) return `'${value.replace(/'/g, `''`)}'`
315    // convert to DB specific null
316    if (typeof value === `undefined` || value === null) return `NULL`
317    return value
318  }).join(`, `)).join(`), (`)
319}
320
321export function parseEnvFromInput (input) {
322  const env = {}
323  for (const key in input) {
324    if (key === key.toUpperCase()) env[key] = input[key]
325  }
326  console.log(`[parseEnvFromInput] ${JSON.stringify(env)}`)
327  Object.assign(process.env, env)
328}