1import { Actor } from 'apify';
2import { PlaywrightCrawler, Dataset } from 'crawlee';
3
4
5await Actor.init();
6
7
8const input = await Actor.getInput() ?? {};
9
10const {
11 pincode = '411001',
12 searchUrls = [],
13 searchQueries = [],
14 maxProductsPerSearch = 100,
15 proxyConfiguration = { useApifyProxy: false },
16 maxRequestRetries = 3,
17 navigationTimeout = 90000,
18 headless = false,
19 screenshotOnError = true,
20 debugMode = false,
21 scrollCount = 5
22} = input;
23
24
25const USER_AGENTS = [
26 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
27 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
28 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15',
29 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36'
30];
31
32
33let locationSetGlobally = false;
34
35
36
37
38
39
40function pickRandom(arr) {
41 return arr[Math.floor(Math.random() * arr.length)];
42}
43
44
45
46
47function parseProxyUrl(proxyUrl) {
48 try {
49 const url = new URL(proxyUrl);
50 const proxy = {
51 server: `${url.protocol}//${url.hostname}${url.port ? `:${url.port}` : ''}`
52 };
53 if (url.username) proxy.username = decodeURIComponent(url.username);
54 if (url.password) proxy.password = decodeURIComponent(url.password);
55 return proxy;
56 } catch (error) {
57 console.error('Invalid proxy URL:', error.message);
58 return null;
59 }
60}
61
62
63
64
65const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
66
67
68
69
70
71
72async function closeLocationPopup(page, log) {
73 try {
74 log.info('🔔 Attempting to close location services popup...');
75
76
77 const popup = page.locator('div.alcohol-popup').first();
78 const popupCount = await popup.count();
79
80 if (popupCount === 0) {
81 log.info('ℹ️ No location popup detected');
82 return true;
83 }
84
85 log.info('📋 Found location services popup - closing...');
86
87
88 const closeButtonSelectors = [
89 'button#btn_location_close_icon',
90 'button.close-privacy',
91 'button.close-icon'
92 ];
93
94 for (const selector of closeButtonSelectors) {
95 try {
96 const closeBtn = page.locator(selector).first();
97 if (await closeBtn.count() > 0) {
98 await closeBtn.click({ timeout: 3000 });
99 log.info(`✓ Clicked close button using: ${selector}`);
100 await delay(800);
101
102
103 const stillVisible = await popup.count();
104 if (stillVisible === 0) {
105 log.info('✅ Location popup closed successfully');
106 return true;
107 }
108 }
109 } catch (error) {
110 continue;
111 }
112 }
113
114
115 try {
116 const selectLocBtn = page.locator('button#select_location_popup').first();
117 if (await selectLocBtn.count() > 0) {
118 await selectLocBtn.click({ timeout: 3000 });
119 log.info('✓ Clicked "Select Location Manually" button');
120 await delay(1000);
121 log.info('✅ Location popup closed via manual selection');
122 return true;
123 }
124 } catch (error) {
125 log.warning(`Failed to click Select Location button: ${error.message}`);
126 }
127
128 log.warning('⚠️ Could not close location popup, but continuing...');
129 return false;
130
131 } catch (error) {
132 log.error(`❌ Error closing popup: ${error.message}`);
133 return false;
134 }
135}
136
137
138
139
140
141
142
143
144
145async function setPincodeLocation(page, log, targetPincode) {
146 try {
147 log.info(`🎯 Setting location to pincode: ${targetPincode}`);
148
149
150 await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
151 await delay(2000);
152
153
154 const locationSelectors = [
155 'button#btn_pin_code_delivery',
156 'button.header-main-pincode-address',
157 'span#delivery_city_pincode'
158 ];
159
160 let locationButtonClicked = false;
161 for (const selector of locationSelectors) {
162 try {
163 const button = page.locator(selector).first();
164 if (await button.isVisible({ timeout: 3000 })) {
165 await button.click({ timeout: 5000 });
166 log.info(`✓ Clicked location button using: ${selector}`);
167 locationButtonClicked = true;
168 break;
169 }
170 } catch (error) {
171 continue;
172 }
173 }
174
175 if (!locationButtonClicked) {
176 log.warning('⚠️ Could not find location button');
177 return false;
178 }
179
180 await delay(2000);
181
182
183 try {
184 await page.waitForSelector('div#delivery_popup', { timeout: 5000, state: 'visible' });
185 log.info('✓ Delivery popup opened');
186 } catch (error) {
187 log.warning('⚠️ Delivery popup did not appear');
188 return false;
189 }
190
191 await delay(1500);
192
193
194
195 try {
196
197 await page.evaluate(() => {
198 const deliveryContent = document.querySelector('#delivery-content');
199 if (deliveryContent && deliveryContent.style.display === 'none') {
200 deliveryContent.style.display = 'block';
201 }
202 });
203 } catch (e) {
204
205 }
206
207 await delay(1000);
208
209
210 try {
211 const enterPincodeBtn = page.locator('button#btn_enter_pincode').first();
212 await enterPincodeBtn.waitFor({ state: 'visible', timeout: 5000 });
213 await enterPincodeBtn.click({ timeout: 5000 });
214 log.info('✓ Clicked "Enter a pincode" button');
215 } catch (error) {
216 log.error(`❌ Could not click "Enter a pincode" button: ${error.message}`);
217 return false;
218 }
219
220 await delay(2000);
221
222
223 try {
224 await page.waitForSelector('div#delivery_enter_pincode', { timeout: 5000, state: 'visible' });
225 log.info('✓ Pincode entry form appeared');
226 } catch (error) {
227 log.error('❌ Pincode entry form did not appear');
228 return false;
229 }
230
231 await delay(1000);
232
233
234 const inputField = page.locator('input#rel_pincode').first();
235
236 try {
237 await inputField.waitFor({ state: 'visible', timeout: 5000 });
238 log.info('✓ Found pincode input field');
239 } catch (error) {
240 log.error('❌ Could not find pincode input field');
241 return false;
242 }
243
244
245 try {
246
247 await inputField.click();
248 await delay(500);
249
250
251 await inputField.fill('');
252 await delay(300);
253
254
255 await inputField.click({ clickCount: 3 });
256 await delay(200);
257
258
259 await inputField.fill(targetPincode);
260 await delay(500);
261
262
263 const inputValue = await inputField.inputValue();
264 log.info(`✓ Entered pincode: ${inputValue}`);
265
266 if (inputValue !== targetPincode) {
267 log.error(`❌ Pincode mismatch! Expected: ${targetPincode}, Got: ${inputValue}`);
268 return false;
269 }
270
271
272 await page.evaluate((pincode) => {
273 const input = document.querySelector('input#rel_pincode');
274 if (input) {
275 input.value = pincode;
276 input.dispatchEvent(new Event('input', { bubbles: true }));
277 input.dispatchEvent(new Event('change', { bubbles: true }));
278 input.dispatchEvent(new Event('blur', { bubbles: true }));
279 }
280 }, targetPincode);
281
282 log.info('✓ Triggered validation events');
283 await delay(2000);
284
285 } catch (error) {
286 log.error(`❌ Failed to enter pincode: ${error.message}`);
287 return false;
288 }
289
290
291 try {
292
293 await page.waitForSelector('div#delivery_pin_msg.field-success', {
294 timeout: 5000,
295 state: 'visible'
296 });
297
298 const locationMessage = await page.locator('div#delivery_pin_msg').textContent();
299 log.info(`✓ Location detected: ${locationMessage.trim()}`);
300 } catch (error) {
301 log.warning('⚠️ Location message not detected, checking if button is enabled...');
302 }
303
304 await delay(1000);
305
306
307 try {
308 const applyButton = page.locator('button#btn_pincode_submit').first();
309
310
311 await applyButton.waitFor({ state: 'visible', timeout: 5000 });
312
313
314 const isDisabled = await applyButton.isDisabled();
315 if (isDisabled) {
316 log.warning('⚠️ Apply button is disabled, attempting to enable it...');
317
318
319 await page.evaluate(() => {
320 const btn = document.querySelector('button#btn_pincode_submit');
321 if (btn) {
322 btn.disabled = false;
323 btn.removeAttribute('disabled');
324 btn.classList.remove('disabled');
325 }
326 });
327
328 await delay(500);
329 }
330
331
332 await applyButton.click({ force: true, timeout: 5000 });
333 log.info('✓ Clicked Apply button');
334
335 } catch (error) {
336 log.error(`❌ Could not click Apply button: ${error.message}`);
337 return false;
338 }
339
340
341 try {
342
343 await page.waitForSelector('div#delivery_popup', {
344 state: 'hidden',
345 timeout: 10000
346 });
347 log.info('✅ Location modal closed successfully');
348 } catch (error) {
349
350 const isStillVisible = await page.locator('div#delivery_popup').isVisible().catch(() => false);
351
352 if (isStillVisible) {
353 log.warning('⚠️ Modal still visible, trying to close manually...');
354
355
356 try {
357 await page.locator('button#close_delivery_popup').click({ timeout: 2000 });
358 await delay(1000);
359 } catch (e) {
360
361 try {
362 await page.locator('div.backdrop').click({ timeout: 2000 });
363 await delay(1000);
364 } catch (e2) {
365 log.error('❌ Could not close modal');
366 return false;
367 }
368 }
369 } else {
370 log.info('✅ Modal closed (not visible)');
371 }
372 }
373
374
375 await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => {});
376 await delay(2000);
377
378
379 try {
380 const headerPincode = await page.locator('button#btn_pin_code_delivery, span#delivery_city_pincode')
381 .first()
382 .textContent()
383 .catch(() => '');
384
385 if (headerPincode.includes(targetPincode)) {
386 log.info(`✅ Location verified in header: ${headerPincode}`);
387 } else {
388 log.info(`ℹ️ Location set, header shows: ${headerPincode}`);
389 }
390 } catch (e) {
391
392 log.info('ℹ️ Could not verify location in header, but proceeding...');
393 }
394
395 log.info('✅ Pincode location set successfully');
396 return true;
397
398 } catch (error) {
399 log.error(`❌ Error in setPincodeLocation: ${error.message}`);
400
401
402 try {
403 const screenshot = await page.screenshot({ fullPage: true });
404 await Actor.setValue(`pincode-error-${Date.now()}.png`, screenshot, {
405 contentType: 'image/png'
406 });
407 log.info('📸 Error screenshot saved');
408 } catch (e) {
409
410 }
411
412 return false;
413 }
414}
415
416
417
418
419
420
421async function autoScroll(page, log, iterations = 5) {
422 try {
423 log.info(`🔄 Starting auto-scroll (${iterations} iterations)...`);
424
425 for (let i = 0; i < iterations; i++) {
426 await page.evaluate(() => window.scrollBy(0, window.innerHeight));
427 await delay(1500);
428 }
429
430
431 await page.evaluate(() => window.scrollTo(0, 0));
432 await delay(500);
433
434 log.info('✓ Auto-scroll completed');
435 return true;
436 } catch (error) {
437 log.warning(`⚠️ Auto-scroll failed: ${error.message}`);
438 return false;
439 }
440}
441
442
443
444
445
446
447async function debugPageState(page, log, label = 'debug') {
448 if (!debugMode) return;
449
450 try {
451 const timestamp = Date.now();
452
453
454 const screenshot = await page.screenshot({ fullPage: true });
455 await Actor.setValue(`${label}-${timestamp}.png`, screenshot, { contentType: 'image/png' });
456
457
458 const html = await page.content();
459 await Actor.setValue(`${label}-${timestamp}.html`, html, { contentType: 'text/html' });
460
461
462 const pageInfo = await page.evaluate(() => ({
463 url: window.location.href,
464 title: document.title,
465 viewport: { width: window.innerWidth, height: window.innerHeight },
466 elementCounts: {
467 productCards: document.querySelectorAll('li.ais-InfiniteHits-item').length,
468 productLinks: document.querySelectorAll('a.plp-card-wrapper').length,
469 images: document.querySelectorAll('img').length,
470 prices: document.querySelectorAll('span.jm-heading-xxs').length
471 }
472 }));
473
474 log.info(`📊 Page state: ${JSON.stringify(pageInfo, null, 2)}`);
475 } catch (error) {
476 log.error(`Debug failed: ${error.message}`);
477 }
478}
479
480
481
482
483
484
485async function waitForSearchResults(page, log) {
486 const selectors = [
487 'li.ais-InfiniteHits-item',
488 'a.plp-card-wrapper',
489 'div.plp-card-container',
490 'div.plp-card-image'
491 ];
492
493 for (const selector of selectors) {
494 try {
495 await page.waitForSelector(selector, { timeout: 10000, state: 'visible' });
496 const count = await page.locator(selector).count();
497 if (count > 0) {
498 log.info(`✓ Found ${count} product elements using: ${selector}`);
499 await delay(1000);
500 return true;
501 }
502 } catch (error) {
503 continue;
504 }
505 }
506
507
508 try {
509 const bodyText = await page.textContent('body');
510 if (bodyText?.includes('₹') || /\bAdd\b/i.test(bodyText)) {
511 log.info('✓ Found product indicators in page content');
512 return true;
513 }
514 } catch (error) {
515
516 }
517
518 log.warning('⚠️ No search results detected');
519 return false;
520}
521
522
523
524
525
526
527async function extractJioMartProducts(page, log) {
528 try {
529 log.info('🔍 Extracting products...');
530
531 const products = await page.evaluate(() => {
532 const extractedProducts = [];
533 const productItems = document.querySelectorAll('li.ais-InfiniteHits-item');
534
535 productItems.forEach((item, index) => {
536 try {
537
538 const productLink = item.querySelector('a.plp-card-wrapper');
539 if (!productLink) return;
540
541 const productUrl = productLink.href;
542 const productId = productLink.getAttribute('data-objid');
543
544
545 const gtmData = item.querySelector('.gtmEvents');
546
547
548 const nameEl = item.querySelector('div.plp-card-details-name');
549 const productName = nameEl?.textContent?.trim() ||
550 gtmData?.getAttribute('data-name') ||
551 productLink.getAttribute('title');
552
553
554 const imgEl = item.querySelector('img.lazyloaded, img.lazyautosizes');
555 let productImage = imgEl?.src || imgEl?.getAttribute('data-src');
556 if (productImage && !productImage.startsWith('http')) {
557 productImage = `https://www.jiomart.com${productImage}`;
558 }
559
560
561 const priceEl = item.querySelector('span.jm-heading-xxs');
562 let currentPrice = null;
563 if (priceEl) {
564 const priceText = priceEl.textContent.trim();
565 const match = priceText.match(/₹\s*(\d+(?:,\d+)*(?:\.\d+)?)/);
566 currentPrice = match ? parseFloat(match[1].replace(/,/g, '')) : null;
567 }
568
569
570 if (!currentPrice && gtmData) {
571 const gtmPrice = gtmData.getAttribute('data-price');
572 currentPrice = gtmPrice ? parseFloat(gtmPrice) : null;
573 }
574
575
576 const originalPriceEl = item.querySelector('span.line-through');
577 let originalPrice = null;
578 if (originalPriceEl) {
579 const priceText = originalPriceEl.textContent.trim();
580 const match = priceText.match(/₹\s*(\d+(?:,\d+)*(?:\.\d+)?)/);
581 originalPrice = match ? parseFloat(match[1].replace(/,/g, '')) : null;
582 }
583
584
585 const discountEl = item.querySelector('span.jm-badge');
586 let discountPercentage = null;
587 if (discountEl) {
588 const discountText = discountEl.textContent.trim();
589 const match = discountText.match(/(\d+)%/);
590 discountPercentage = match ? parseInt(match[1]) : null;
591 }
592
593
594 if (!discountPercentage && currentPrice && originalPrice && originalPrice > currentPrice) {
595 discountPercentage = Math.round(((originalPrice - currentPrice) / originalPrice) * 100);
596 }
597
598
599 let productWeight = null;
600 const weightMatch = productName?.match(/(\d+\s*(?:g|kg|ml|l|gm|pack|pcs|piece))/i);
601 if (weightMatch) {
602 productWeight = weightMatch[1];
603 }
604
605
606 const brand = gtmData?.getAttribute('data-manu') || null;
607
608
609 const vegIcon = item.querySelector('img[src*="icon-veg"]');
610 const isVegetarian = vegIcon !== null;
611
612
613 const addButton = item.querySelector('button.addtocartbtn');
614 const isOutOfStock = addButton?.hasAttribute('disabled') || false;
615
616 if (productName && currentPrice) {
617 extractedProducts.push({
618 productId: productId || `jiomart-${index}`,
619 productName,
620 productImage,
621 currentPrice,
622 originalPrice: originalPrice || currentPrice,
623 discountPercentage: discountPercentage || 0,
624 productWeight,
625 brand,
626 isVegetarian,
627 isOutOfStock,
628 productUrl,
629 scrapedAt: new Date().toISOString()
630 });
631 }
632 } catch (error) {
633 console.error(`Error extracting product ${index}:`, error);
634 }
635 });
636
637 return extractedProducts;
638 });
639
640 log.info(`✅ Extracted ${products.length} products`);
641
642 if (debugMode && products.length > 0) {
643 log.info(`Sample product: ${JSON.stringify(products[0], null, 2)}`);
644 }
645
646 return products;
647 } catch (error) {
648 log.error(`❌ Error extracting products: ${error.message}`);
649 return [];
650 }
651}
652
653
654
655const proxyConfig = proxyConfiguration?.useApifyProxy
656 ? await Actor.createProxyConfiguration(proxyConfiguration)
657 : undefined;
658
659const customProxyUrl = proxyConfiguration?.customProxyUrl ||
660 proxyConfiguration?.proxyUrl ||
661 proxyConfiguration?.proxy;
662const launchProxy = customProxyUrl ? parseProxyUrl(customProxyUrl) : null;
663
664
665
666const allSearchUrls = [
667 ...searchUrls,
668 ...searchQueries.map(query =>
669 `https://www.jiomart.com/search?q=${encodeURIComponent(query)}`
670 )
671];
672
673
674
675const crawler = new PlaywrightCrawler({
676 proxyConfiguration: proxyConfig,
677 maxRequestRetries,
678 navigationTimeoutSecs: navigationTimeout / 1000,
679 headless:false,
680
681 launchContext: {
682 launchOptions: {
683 args: [
684 '--disable-blink-features=AutomationControlled',
685 '--disable-dev-shm-usage',
686 '--no-sandbox',
687 '--disable-setuid-sandbox',
688 '--disable-web-security',
689 '--disable-features=IsolateOrigins,site-per-process'
690 ],
691 ...((!proxyConfig && launchProxy) ? { proxy: launchProxy } : {})
692 }
693 },
694
695 preNavigationHooks: [
696 async ({ page, log }) => {
697 try {
698 const userAgent = pickRandom(USER_AGENTS);
699
700 await page.setExtraHTTPHeaders({
701 'Accept-Language': 'en-US,en;q=0.9',
702 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
703 'User-Agent': userAgent,
704 'Cache-Control': 'no-cache',
705 'Pragma': 'no-cache'
706 });
707
708 await page.setViewportSize({ width: 1920, height: 1080 });
709
710
711 await page.addInitScript((ua) => {
712 Object.defineProperty(navigator, 'webdriver', { get: () => false });
713 Object.defineProperty(navigator, 'userAgent', { get: () => ua });
714 Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
715 Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
716
717
718 window.chrome = { runtime: {} };
719 }, userAgent).catch(() => {});
720
721 } catch (error) {
722 log.error(`preNavigationHook error: ${error.message}`);
723 }
724 }
725 ],
726
727 async requestHandler({ page, request, log }) {
728 const { url } = request;
729 const isFirstRequest = request.userData?.isFirst || false;
730
731 log.info(`🔍 Processing: ${url}`);
732
733 try {
734
735 if (isFirstRequest) {
736 await closeLocationPopup(page, log);
737 await delay(1000);
738 }
739
740
741 if (isFirstRequest && !locationSetGlobally) {
742 log.info('🎯 First request - setting pincode location');
743
744 const locationSet = await setPincodeLocation(page, log, pincode);
745
746 if (locationSet) {
747 locationSetGlobally = true;
748 log.info('✅ Location set successfully');
749
750
751 await page.reload({ waitUntil: 'networkidle' });
752 await delay(3000);
753 } else {
754 log.warning('⚠️ Failed to set location, continuing anyway');
755 }
756 }
757
758
759 await page.waitForLoadState('domcontentloaded');
760 await delay(3000);
761
762 if (isFirstRequest) {
763 await debugPageState(page, log, 'initial');
764 }
765
766
767 try {
768
769 const closeBtn = page.locator('button#close_delivery_popup').first();
770 if (await closeBtn.count() > 0 && await closeBtn.isVisible({ timeout: 2000 })) {
771 await closeBtn.click();
772 await delay(500);
773 }
774 } catch (error) {
775
776 }
777
778
779 try {
780 const backdrop = page.locator('div.backdrop').first();
781 if (await backdrop.isVisible({ timeout: 2000 })) {
782 await backdrop.click();
783 await delay(500);
784 }
785 } catch (error) {
786
787 }
788
789
790 const resultsFound = await waitForSearchResults(page, log);
791 if (!resultsFound) {
792 await debugPageState(page, log, 'no-results');
793 log.warning('⚠️ No search results detected');
794 }
795
796
797 await autoScroll(page, log, scrollCount);
798
799 if (isFirstRequest) {
800 await debugPageState(page, log, 'after-scroll');
801 }
802
803
804 const products = await extractJioMartProducts(page, log);
805
806 if (products.length === 0) {
807 log.error('❌ No products extracted');
808 await debugPageState(page, log, 'no-products');
809 return;
810 }
811
812
813 const urlParams = new URL(url).searchParams;
814 const searchQuery = urlParams.get('q') || urlParams.get('query') || 'direct_url';
815
816 const productsToSave = products.slice(0, maxProductsPerSearch).map(product => ({
817 ...product,
818 searchQuery,
819 searchUrl: url,
820 platform: 'JioMart',
821 pincode
822 }));
823
824 await Dataset.pushData(productsToSave);
825
826 log.info(`✅ Saved ${productsToSave.length} products for "${searchQuery}" (Pincode: ${pincode})`);
827
828 } catch (error) {
829 log.error(`❌ Error processing ${url}: ${error.message}`);
830
831 if (screenshotOnError) {
832 try {
833 const screenshot = await page.screenshot({ fullPage: true });
834 const timestamp = Date.now();
835 await Actor.setValue(`error-${timestamp}.png`, screenshot, { contentType: 'image/png' });
836 } catch (e) {
837 log.error(`Screenshot failed: ${e.message}`);
838 }
839 }
840
841 throw error;
842 }
843 },
844
845 failedRequestHandler: async ({ request, log }) => {
846 log.error(`❌ Request failed: ${request.url}`);
847
848 const failedUrls = await Actor.getValue('FAILED_URLS') || [];
849 failedUrls.push({
850 url: request.url,
851 timestamp: new Date().toISOString(),
852 error: request.errorMessages?.join(', ')
853 });
854 await Actor.setValue('FAILED_URLS', failedUrls);
855 }
856});
857
858
859
860if (allSearchUrls.length > 0) {
861 console.log('\n' + '='.repeat(60));
862 console.log('🚀 JIOMART SCRAPER STARTED');
863 console.log('='.repeat(60));
864 console.log(`📍 Pincode: ${pincode}`);
865 console.log(`🔍 Search URLs: ${allSearchUrls.length}`);
866 console.log(`📊 Max products per search: ${maxProductsPerSearch}`);
867 console.log(`📜 Scroll iterations: ${scrollCount}`);
868 console.log(`🐛 Debug mode: ${debugMode}`);
869 console.log(`👁️ Headless: ${headless}`);
870 console.log('='.repeat(60) + '\n');
871
872 const searchRequests = allSearchUrls.map((url, index) => ({
873 url,
874 userData: { isFirst: index === 0 }
875 }));
876
877 console.log('🔍 URLs to process:\n');
878 searchRequests.forEach((req, idx) => {
879 console.log(` ${idx + 1}. ${req.url}${idx === 0 ? ' 📍 (will set location)' : ''}`);
880 });
881 console.log('');
882
883 await crawler.run(searchRequests);
884
885 console.log('\n' + '='.repeat(60));
886 console.log('✅ SCRAPING COMPLETED');
887 console.log('='.repeat(60));
888 console.log('📁 Results: storage/datasets/default/');
889 console.log('📸 Debug files: storage/key_value_stores/default/');
890 console.log('='.repeat(60) + '\n');
891} else {
892 console.log('\n' + '='.repeat(60));
893 console.log('❌ NO SEARCH URLS PROVIDED');
894 console.log('='.repeat(60));
895 console.log('Please provide either "searchUrls" or "searchQueries" in input.json');
896 console.log('='.repeat(60) + '\n');
897}
898
899await Actor.exit();