#!/usr/bin/env bun import { mkdirSync, writeFileSync } from 'fs' import { dirname, join } from 'path' const root = process.cwd() const rawRoot = join(root, 'data', 'sources', 'apify', 'raw') const token = Bun.env.APIFY_TOKEN || Bun.env.APIFY_API_TOKEN const generatedAt = new Date().toISOString() if (!token) throw new Error('Missing APIFY_TOKEN or APIFY_API_TOKEN') const maxReviews = Number(Bun.env.APIFY_AMAZON_JUNGLEE_MAX_REVIEWS || 5) const maxTotalChargeUsd = Number(Bun.env.APIFY_AMAZON_JUNGLEE_MAX_USD || 0.5) const onlySkus = (Bun.env.APIFY_AMAZON_JUNGLEE_ONLY || '') .split(',') .map(value => value.trim()) .filter(Boolean) const products = [ { sku: 'JV-TURMERIC500', asin: 'B009756A58', url: 'https://www.amazon.co.uk/dp/B009756A58', context: 'exact_jv_listing', note: 'Exact ASIN from local DataForSEO asin-map.csv.' }, { sku: 'JV-DEFF1000', asin: 'B09LR671LT', url: 'https://www.amazon.co.uk/dp/B09LR671LT', context: 'closest_effervescent_competitor', note: 'Closest Amazon effervescent vitamin D competitor from local DataForSEO product search; not JV product truth.' }, { sku: 'JV-VISISOFT', asin: 'B009756BHA', url: 'https://www.amazon.co.uk/dp/B009756BHA', context: 'exact_jv_listing', note: 'Exact ASIN from local DataForSEO asin-map.csv.' } ].filter(product => onlySkus.length === 0 || onlySkus.includes(product.sku)) if (!products.length) throw new Error('No products selected for Amazon review capture.') function writeJson(path: string, data: any) { mkdirSync(dirname(path), { recursive: true }) writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf8') } async function apify(path: string, options: RequestInit = {}) { const sep = path.includes('?') ? '&' : '?' const res = await fetch(`https://api.apify.com${path}${sep}token=${encodeURIComponent(token!)}`, { ...options, headers: { 'Content-Type': 'application/json', ...(options.headers || {}) } }) const text = await res.text() let data: any try { data = text ? JSON.parse(text) : null } catch { data = text } if (!res.ok) throw new Error(`Apify ${res.status} ${res.statusText}: ${typeof data === 'string' ? data : JSON.stringify(data)}`) return data } async function waitForRun(runId: string, timeoutMs = 1000 * 60 * 12) { const started = Date.now() let run = (await apify(`/v2/actor-runs/${runId}`)).data while (!['SUCCEEDED', 'FAILED', 'ABORTED', 'TIMED-OUT'].includes(run.status)) { if (Date.now() - started > timeoutMs) throw new Error(`Timed out waiting for ${runId}`) await new Promise(resolve => setTimeout(resolve, 5000)) run = (await apify(`/v2/actor-runs/${runId}`)).data console.log(`${runId}: ${run.status} usd=${run.usageTotalUsd || 0}`) } return run } const input = { productUrls: products.map(product => ({ url: product.url })), maxReviews, includeGdprSensitive: false, sort: 'recent', filterByRatings: ['allStars'], reviewsUseProductVariantFilter: false, scrapeProductDetails: false, reviewsAlwaysSaveCategoryData: true, deduplicateRedirectedAsins: true } console.log(`Starting junglee/amazon-reviews-scraper for ${products.length} exact ASIN URLs, maxReviews=${maxReviews}, cap=$${maxTotalChargeUsd}`) const start = await apify(`/v2/acts/junglee~amazon-reviews-scraper/runs?maxTotalChargeUsd=${maxTotalChargeUsd}`, { method: 'POST', body: JSON.stringify(input) }) const run = await waitForRun(start.data.id) const items = run.defaultDatasetId ? await apify(`/v2/datasets/${run.defaultDatasetId}/items?clean=true`) : [] const result = { capturedAt: generatedAt, job: 'amazon-reviews-junglee', actor: 'junglee/amazon-reviews-scraper', note: 'Capped Amazon review capture for exact ASINs only. JV-DEFF1000 is excluded because no exact ASIN is confirmed.', maxTotalChargeUsd, input, productMap: products, run: { id: run.id, status: run.status, statusMessage: run.statusMessage, defaultDatasetId: run.defaultDatasetId, startedAt: run.startedAt, finishedAt: run.finishedAt, usageTotalUsd: run.usageTotalUsd, usage: run.usage, stats: run.stats, chargedEventCounts: run.chargedEventCounts }, itemCount: Array.isArray(items) ? items.length : 0, items: Array.isArray(items) ? items.map((item: any) => { const raw = JSON.stringify(item) const matched = products.find(product => raw.includes(product.asin) || raw.includes(product.url)) return matched ? { ...item, sku: matched.sku, matchedAsin: matched.asin, matchContext: matched.context, matchNote: matched.note } : item }) : items } const stamp = generatedAt.replace(/[:.]/g, '-') writeJson(join(rawRoot, `top3-specialist-amazon-reviews-junglee-${stamp}.json`), result) writeJson(join(rawRoot, 'top3-specialist-amazon-reviews-junglee-latest.json'), result) console.log(`Amazon reviews junglee ${run.status}: ${result.itemCount} items, usd=${run.usageTotalUsd || 0}`)