1import { Actor, log } from 'apify';
2import { CheerioCrawler } from 'crawlee';
3
4import { RobotsGate } from './src/core/robots.js';
5import { BlockTracker, hasBlockMarker } from './src/core/blockTracker.js';
6import { crawlerLimits, jitterDelay, fetchWithRetry, LIMITS } from './src/core/politeness.js';
7import { EVENTBRITE_POLICY, eventIdFromUrl } from './src/targets/eventbrite/urlPolicy.js';
8import { parseEventPage, isPastEvent } from './src/targets/eventbrite/parser.js';
9import { EVENT_SCHEMA } from './src/targets/eventbrite/schema.js';
10import { validateRecord } from './src/core/schema.js';
11import {
12 PROBE_URL, parseBrowserInfo, proxyVerdict, chargeVerdict, reconcileCounters,
13} from './src/core/preflight.js';
14import {
15 shardUrlsFromIndex, extractLocs, gunzipMaybe, parseBrowseUrl, couldMatchCity, resolveCity,
16 selectBrowseUrls, listPagesNeeded, estimateRequests, parseDateInput, inDateRange,
17 shortfallMessage, slugify, linkStockTarget, listsToTopUp, orderBrowseUrls, availableCategories,
18 discoveryVerdict,
19} from './src/targets/eventbrite/discovery.js';
20
21const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0 Safari/537.36';
22const BASE = 'https://www.eventbrite.com';
23const SITEMAP_INDEX = `${BASE}/sitemap_xml/sitemap_index.xml`;
24
25await Actor.init();
26
27const input = (await Actor.getInput()) ?? {};
28const {
29 startUrls = [],
30 city = null,
31 category = null,
32 subcategory = null,
33 onlineOnly = false,
34 maxItems = 50,
35 futureOnly = true,
36 dateFrom = null,
37 dateTo = null,
38} = input;
39
40
41const dateFromD = parseDateInput(dateFrom);
42const dateToD = /^\d{4}-\d{2}-\d{2}$/.test(String(dateTo ?? ''))
43 ? parseDateInput(`${dateTo}T23:59:59.999Z`)
44 : parseDateInput(dateTo);
45if (dateFrom && !dateFromD) await Actor.fail(`dateFrom ayristirilamadi: "${dateFrom}" (YYYY-MM-DD bekleniyor).`);
46if (dateTo && !dateToD) await Actor.fail(`dateTo ayristirilamadi: "${dateTo}" (YYYY-MM-DD bekleniyor).`);
47if (dateFromD && dateToD && dateFromD > dateToD) await Actor.fail('dateFrom, dateTo`dan sonra olamaz.');
48const dateFilter = Boolean(dateFromD || dateToD);
49
50
51const seeds = [];
52for (const s of startUrls) {
53 const url = typeof s === 'string' ? s : s?.url;
54 if (url) seeds.push(url);
55}
56const cityInput = onlineOnly ? 'online' : (city ? String(city).trim() : null);
57let mode;
58if (seeds.length > 0) mode = 'startUrls';
59else if (cityInput) mode = 'discovery';
60else await Actor.fail('Girdi yok: city (ya da onlineOnly) veya startUrls verin.');
61
62
63
64const linkTarget = linkStockTarget(maxItems, { dateFilter, futureOnly });
65const plan = mode === 'discovery'
66 ? estimateRequests(maxItems, { linkTarget })
67 : { fixed: 0, lists: seeds.filter((u) => !/\/e\//.test(u)).length, events: maxItems, total: seeds.length + maxItems };
68const hardCap = Math.max(plan.total * 2, plan.total + 10);
69log.info(`Mod: ${mode}. Plan: ${JSON.stringify(plan)}. Istek tavani: ${hardCap}.`);
70
71
72const allowedSeeds = [];
73for (const u of seeds) {
74 const c = EVENTBRITE_POLICY.check(u);
75 if (c.ok) allowedSeeds.push(u);
76 else log.warning(`URL politikasi reddetti, atlaniyor: ${u} (${c.reason})`);
77}
78if (mode === 'startUrls' && allowedSeeds.length === 0) {
79 await Actor.fail('Tum giris URL`leri politika tarafindan reddedildi.');
80}
81
82
83
84
85const robots = new RobotsGate({
86 userAgent: UA,
87 failClosed: true,
88 log,
89 fetchFn: (u) => fetchWithRetry(u, { init: { headers: { 'user-agent': UA } }, log }),
90});
91
92let crawler;
93const blocks = new BlockTracker({
94 maxConsecutive: 5,
95 log,
96 onAbort: async (stats) => {
97 log.error(`Engel esigi asildi, tarama durduruluyor: ${JSON.stringify(stats)}`);
98 await crawler?.autoscaledPool?.abort();
99 },
100});
101
102const counters = {
103 pushed: 0, skippedPast: 0, dateFiltered: 0, dropped: 0, forbidden: 0, parseFail: 0,
104 policyBlocked: 0, robotsBlocked: 0, piiMasked: 0, truncated: 0, organizerRejected: 0,
105 stripped: 0, depthTrimmed: 0, charged: 0, schemaFail: 0, pushFail: 0,
106 eventPages: 0, duplicate: 0, overMaxItems: 0,
107 sitemapIndex: 0, sitemapIndexFail: 0, sitemapShards: 0, sitemapShardFail: 0, listPages: 0, listPagesEmpty: 0,
108 eventLinksFound: 0, eventLinksEnqueued: 0, dBlocked: 0,
109};
110const schemaViolations = new Set();
111let chargeLimitReached = false;
112const egress = { direct: null, proxied: null };
113
114
115const disc = {
116 citySlug: cityInput ? slugify(cityInput) : null,
117 shardsTotal: 0,
118 shardsDone: 0,
119 allCities: new Set(),
120 candidates: [],
121 resolved: null,
122 matched: [],
123 matchedEnqueued: 0,
124 listsEnqueued: 0,
125 finalized: false,
126};
127const seenIds = new Set();
128const eventLinks = new Set();
129const eventLinksNeeded = () => linkTarget;
130
131
132async function mayVisit(url) {
133 const pol = EVENTBRITE_POLICY.check(url);
134 if (!pol.ok) { counters.policyBlocked += 1; return false; }
135 if (!(await robots.isAllowed(url))) { counters.robotsBlocked += 1; return false; }
136 return true;
137}
138
139function labelFor(url) {
140 const p = new URL(url).pathname;
141 if (/^\/e\//.test(p)) return 'EVENT';
142 if (/^\/sitemap_xml\//.test(p)) return 'SITEMAP_INDEX';
143 return 'LIST';
144}
145
146async function enqueueLists(n) {
147 let eklendi = 0;
148 while (eklendi < n && disc.matchedEnqueued < disc.matched.length) {
149 const u = disc.matched[disc.matchedEnqueued];
150 disc.matchedEnqueued += 1;
151 if (!(await mayVisit(u))) continue;
152 await crawler.addRequests([{ url: u, label: 'LIST' }]);
153 eklendi += 1;
154 }
155 disc.listsEnqueued += eklendi;
156 return eklendi;
157}
158
159
160
161
162
163async function topUpLists() {
164 const pending = disc.listsEnqueued - counters.listPages;
165 const observedYield = counters.listPages ? counters.eventLinksFound / counters.listPages : 0;
166 const n = listsToTopUp({
167 stock: eventLinks.size,
168 needed: eventLinksNeeded(),
169 pending,
170 observedYield,
171 available: disc.matched.length - disc.matchedEnqueued,
172 });
173 if (n > 0) await enqueueLists(n);
174}
175
176
177async function finalizeDiscovery() {
178 if (disc.finalized) return;
179 disc.finalized = true;
180
181 if (counters.sitemapShards === 0) {
182 await Actor.fail(`KESIF BASLAYAMADI: ${disc.shardsTotal} sitemap parcasinin hicbiri alinamadi (${counters.sitemapShardFail} basarisiz).`);
183 }
184 const r = resolveCity(cityInput, disc.allCities);
185 if (!r.ok) {
186 await Actor.fail(
187 `Sehir cozulemedi: ${r.reason}. ` +
188 (r.candidates.length ? `Adaylar: ${r.candidates.join(', ')}. Tam slug verin.` : 'Aday yok.'),
189 );
190 }
191 disc.resolved = r;
192
193 disc.matched = orderBrowseUrls(
194 selectBrowseUrls(disc.candidates, { citySlugs: r.slugs, category, subcategory }),
195 r.base,
196 );
197 const kapsam = `${r.base}${category ? `/${slugify(category)}` : ''}${subcategory ? `/${slugify(subcategory)}` : ''}`;
198 log.info(`Sehir: "${cityInput}" -> ${r.base} (${r.slugs.length} slug). ${kapsam} icin eslesen /b/ sayfasi: ${disc.matched.length}.`);
199 if (disc.matched.length === 0) {
200
201 const kategoriler = availableCategories(disc.candidates, { citySlugs: r.slugs });
202 let ipucu;
203 if (category && kategoriler.includes(slugify(category))) {
204 const altlar = availableCategories(disc.candidates, { citySlugs: r.slugs, category });
205 ipucu = `"${slugify(category)}" gecerli ama alt kategori degil. Gecerli alt kategoriler (${altlar.length}): ${altlar.join(', ') || 'yok'}.`;
206 } else {
207 ipucu = `Gecerli kategoriler (${kategoriler.length}): ${kategoriler.join(', ') || 'yok'}.`;
208 }
209 await Actor.fail(`${kapsam} icin sitemap'te /b/ sayfasi yok. ${ipucu}`);
210 }
211 const ilkParti = listPagesNeeded(eventLinksNeeded());
212 const eklendi = await enqueueLists(ilkParti);
213 log.info(`Ilk parti: ${eklendi} /b/ sayfasi kuyruga alindi (plan ${ilkParti}, eslesen ${disc.matched.length}).`);
214}
215
216const proxyConfiguration = await Actor.createProxyConfiguration();
217const isAtHome = Actor.isAtHome();
218log.info(`Ortam: isAtHome=${isAtHome}, proxyConfiguration=${proxyConfiguration ? 'var' : 'YOK'}`);
219
220
221try {
222 const res = await fetch(PROBE_URL, { headers: { 'user-agent': UA } });
223 egress.direct = parseBrowserInfo(await res.text());
224} catch (e) {
225 log.warning(`Dogrudan cikis IP'si okunamadi: ${e.message}`);
226}
227
228crawler = new CheerioCrawler({
229 ...crawlerLimits(),
230 proxyConfiguration,
231 maxRequestsPerCrawl: hardCap,
232 maxSessionRotations: 2,
233 requestHandlerTimeoutSecs: 60,
234 navigationTimeoutSecs: 45,
235 additionalMimeTypes: [
236 'application/xml', 'text/xml',
237 'application/gzip', 'application/x-gzip', 'application/octet-stream', 'binary/octet-stream',
238 ],
239 preNavigationHooks: [
240 async ({ request }) => {
241 const sitemap = request.label === 'SITEMAP_INDEX' || request.label === 'SITEMAP_SHARD';
242 request.headers = {
243 ...request.headers,
244 'user-agent': UA,
245 accept: sitemap ? '*/*' : 'text/html,application/xhtml+xml,*/*',
246 'accept-language': 'en-US,en;q=0.9',
247 };
248 await jitterDelay(LIMITS.minDelayMs, LIMITS.maxDelayMs);
249 },
250 ],
251
252 async requestHandler({ request, body, $, proxyInfo }) {
253
254 if (request.label === 'PROBE') {
255 egress.proxied = parseBrowserInfo(body?.toString?.() ?? '');
256 log.info(`Proxy olcumu: ${JSON.stringify(egress.proxied)} | proxyInfo.hostname=${proxyInfo?.hostname ?? 'yok'} sessionId=${proxyInfo?.sessionId ?? 'yok'}`);
257 return;
258 }
259
260
261 if (request.label === 'SITEMAP_SHARD') {
262 const xml = gunzipMaybe(body);
263 const locs = extractLocs(xml);
264 counters.sitemapShards += 1;
265 disc.shardsDone += 1;
266 if (locs.length === 0) log.warning(`Parca bos ya da acilamadi: ${request.url}`);
267 let aday = 0;
268 for (const loc of locs) {
269 const p = parseBrowseUrl(loc);
270 if (!p) continue;
271 disc.allCities.add(p.city);
272 if (couldMatchCity(p.city, disc.citySlug)) { disc.candidates.push(loc); aday += 1; }
273 }
274 log.info(`Parca ${disc.shardsDone}/${disc.shardsTotal}: ${locs.length} URL, ${aday} sehir adayi.`);
275 if (disc.shardsDone >= disc.shardsTotal) await finalizeDiscovery();
276 return;
277 }
278
279 const bodyText = body?.toString?.() ?? '';
280 if (blocks.recordResponse({ status: 200, body: bodyText, url: request.url })) {
281 return;
282 }
283
284
285 if (request.label === 'SITEMAP_INDEX') {
286 const shards = shardUrlsFromIndex(bodyText);
287 counters.sitemapIndex += 1;
288 if (shards.length === 0) {
289 await Actor.fail('Sitemap indeksinde category_browse_pages parcasi yok; kesif yapilamiyor.');
290 }
291 const istekler = [];
292 for (const u of shards) if (await mayVisit(u)) istekler.push({ url: u, label: 'SITEMAP_SHARD' });
293 disc.shardsTotal = istekler.length;
294 await crawler.addRequests(istekler);
295 log.info(`Sitemap indeksi: ${shards.length} category_browse_pages parcasi, ${istekler.length} kuyruga alindi.`);
296 return;
297 }
298
299
300 if (request.label === 'LIST') {
301 const path = new URL(request.url).pathname;
302 const hrefs = new Set();
303 $('a[href]').each((_, el) => {
304 const href = $(el).attr('href');
305 if (!href) return;
306 let abs;
307 try { abs = new URL(href, request.url).href.split('#')[0].split('?')[0]; } catch { return; }
308 if (/\/e\/[^/?#]+/.test(new URL(abs).pathname)) hrefs.add(abs);
309 });
310 counters.listPages += 1;
311 if (hrefs.size === 0) counters.listPagesEmpty += 1;
312 if (/^\/o\//.test(path)) {
313 log.warning(`Organizator sayfasi statik HTML'de etkinlik tasimiyor (${hrefs.size} bag): ${request.url}`);
314 }
315 let queued = 0;
316 for (const href of hrefs) {
317 if (eventLinks.has(href)) continue;
318 counters.eventLinksFound += 1;
319 if (eventLinks.size >= eventLinksNeeded()) continue;
320 if (!(await mayVisit(href))) continue;
321 eventLinks.add(href);
322
323 await crawler.addRequests([{ url: href, label: 'EVENT', userData: { sourceListUrl: request.url } }]);
324 queued += 1;
325 }
326 counters.eventLinksEnqueued += queued;
327 log.info(`Liste ${path}: ${hrefs.size} etkinlik bagi, ${queued} kuyruga alindi (stok ${eventLinks.size}/${eventLinksNeeded()}).`);
328 if (mode === 'discovery') await topUpLists();
329 return;
330 }
331
332
333
334 counters.eventPages += 1;
335 const parsed = parseEventPage(bodyText, request.url);
336 if (!parsed.ok) {
337 counters.parseFail += 1;
338 log.warning(`Ayristirilamadi (${parsed.reason}): ${request.url}`);
339 return;
340 }
341
342 counters.dropped += parsed.dropped.length;
343 counters.truncated += parsed.truncated.length;
344 counters.piiMasked += parsed.meta.piiMasked;
345 if (parsed.forbiddenHits.length) {
346 counters.forbidden += parsed.forbiddenHits.length;
347 log.error(`YASAKLI ALAN: ${parsed.forbiddenHits.join(', ')} @ ${request.url}`);
348 }
349
350 if (parsed.stripped.length) {
351 counters.stripped += parsed.stripped.length;
352 log.warning(`KAYITTAN DUSURULDU (yasakli, derin): ${parsed.stripped.join(', ')} @ ${request.url}`);
353 }
354 if (parsed.depthTrimmed.length) {
355 counters.depthTrimmed += parsed.depthTrimmed.length;
356 log.warning(`KAYITTAN DUSURULDU (derinlik siniri): ${parsed.depthTrimmed.join(', ')} @ ${request.url}`);
357 }
358 if (parsed.meta.piiMasked > 0) {
359 log.info(`PII maskelendi (${JSON.stringify(parsed.meta.piiKinds)}) @ ${request.url}`);
360 }
361 if (parsed.meta.organizerRejected) {
362 counters.organizerRejected += 1;
363 log.info(`Organizator @type=${parsed.meta.organizerRejectedType}, ad alinmadi: ${request.url}`);
364 }
365
366
367 const rec = { ...parsed.record, sourceListUrl: request.userData?.sourceListUrl ?? null };
368 if (futureOnly && isPastEvent(rec)) { counters.skippedPast += 1; return; }
369 if (!inDateRange(rec, dateFromD, dateToD)) { counters.dateFiltered += 1; return; }
370
371 const id = rec.id ?? eventIdFromUrl(request.url) ?? request.url;
372 if (seenIds.has(id)) { counters.duplicate += 1; return; }
373 seenIds.add(id);
374
375 if (counters.pushed >= maxItems) { counters.overMaxItems += 1; return; }
376
377
378
379 const violations = validateRecord(rec, EVENT_SCHEMA);
380 if (violations.length) {
381 counters.schemaFail += 1;
382 violations.forEach((v) => schemaViolations.add(v));
383 seenIds.delete(id);
384 log.error(`SEMA UYUSMAZLIGI (${request.url}): ${violations.join('; ')}`);
385 return;
386 }
387
388
389 let charge;
390 try {
391 charge = await Actor.pushData(rec, 'result');
392 } catch (e) {
393
394
395 seenIds.delete(id);
396 const semaHatasi = /schema validation/i.test(e?.message ?? '');
397 if (semaHatasi) {
398 counters.schemaFail += 1;
399 schemaViolations.add(e.message);
400 log.error(`PLATFORM SEMA HATASI (${request.url}): ${e.message}`);
401 return;
402 }
403 counters.pushFail += 1;
404 log.error(`DATASET YAZMA HATASI (${request.url}): ${e?.message ?? e}`);
405 throw e;
406 }
407 counters.pushed += 1;
408 counters.charged += charge?.chargedCount ?? 0;
409 if (charge?.eventChargeLimitReached) {
410 chargeLimitReached = true;
411 log.warning('Kullanicinin harcama tavani doldu, tarama durduruluyor.');
412 await crawler.autoscaledPool?.abort();
413 return;
414 }
415
416 if (counters.pushed >= maxItems) {
417 log.info(`maxItems (${maxItems}) doldu, durduruluyor.`);
418 await crawler.autoscaledPool?.abort();
419 }
420 },
421
422 async failedRequestHandler({ request, response }) {
423 const status = response?.statusCode ?? 0;
424 const marker = hasBlockMarker(response?.body);
425 blocks.recordResponse({ status, body: response?.body, url: request.url });
426 const path = new URL(request.url).pathname;
427 if (status === 405 || marker === 'human verification') {
428 if (/^\/d\//.test(path)) {
429 counters.dBlocked += 1;
430 log.warning(`Liste yolu (/d/) engellendi, yeniden DENENMEYECEK: ${request.url}. Bu yol guvenilir degil; sehir/kategori girdisini ya da /b/ kullanin.`);
431 } else {
432 log.warning(`405/insan dogrulamasi, yeniden DENENMEYECEK: ${request.url}`);
433 }
434 return;
435 }
436 if (request.label === 'SITEMAP_SHARD') {
437 disc.shardsDone += 1;
438 counters.sitemapShardFail += 1;
439 log.error(`Sitemap parcasi alinamadi (${status}): ${request.url}`);
440 if (disc.shardsDone >= disc.shardsTotal) await finalizeDiscovery();
441 return;
442 }
443 if (request.label === 'SITEMAP_INDEX') {
444 counters.sitemapIndexFail += 1;
445 log.error(`Sitemap indeksi alinamadi (${status}): ${request.url} -- kesif baslayamiyor`);
446 return;
447 }
448 log.warning(`Istek basarisiz (${status}): ${request.url}`);
449 },
450});
451
452
453const finalSeeds = [];
454if (mode === 'discovery') {
455 if (await mayVisit(SITEMAP_INDEX)) finalSeeds.push({ url: SITEMAP_INDEX, label: 'SITEMAP_INDEX' });
456 else await Actor.fail('robots.txt sitemap indeksine izin vermiyor; kesif yapilamiyor.');
457} else {
458 for (const u of allowedSeeds) {
459 if (!(await mayVisit(u))) { log.warning(`robots.txt reddetti, atlaniyor: ${u}`); continue; }
460 const label = labelFor(u);
461 const req = { url: u, label };
462 if (/^\/d\//.test(new URL(u).pathname)) req.noRetry = true;
463 if (label === 'EVENT') eventLinks.add(u.split('#')[0].split('?')[0]);
464 finalSeeds.push(req);
465 }
466 if (finalSeeds.length === 0) await Actor.fail('Tum giris URL`leri robots.txt ya da politika tarafindan reddedildi.');
467}
468
469log.info(`Baslaniyor: mod=${mode}, ${finalSeeds.length} tohum, maxItems=${maxItems}, futureOnly=${futureOnly}, tarih=${dateFrom ?? '-'}..${dateTo ?? '-'}`);
470await crawler.run([{ url: PROBE_URL, label: 'PROBE' }, ...finalSeeds]);
471
472
473const st = crawler.stats?.state ?? {};
474const actualRequests = (st.requestsFinished ?? 0) + (st.requestsFailed ?? 0) - 1;
475const hardCapHit = actualRequests + 1 >= hardCap;
476const budget = {
477 plan,
478 hardCap,
479 actual: actualRequests,
480 actualPerRecord: counters.pushed ? Number((actualRequests / counters.pushed).toFixed(2)) : null,
481 planPerRecord: Number((plan.total / plan.events).toFixed(2)),
482 hardCapHit,
483};
484
485
486const proxyCheck = proxyVerdict({
487 direct: egress.direct, proxied: egress.proxied, isAtHome, proxyConfigured: Boolean(proxyConfiguration),
488});
489const chargeCheck = chargeVerdict({
490 isAtHome, pushed: counters.pushed, chargedCount: counters.charged, limitReached: chargeLimitReached,
491});
492
493
494const kesifHukmu = mode === 'discovery'
495 ? discoveryVerdict({
496 sitemapIndex: counters.sitemapIndex,
497 indexFailed: counters.sitemapIndexFail,
498 shardsTotal: disc.shardsTotal,
499 shardsDone: disc.shardsDone,
500 shardsFailed: counters.sitemapShardFail,
501 })
502 : { ok: true, partial: false, reason: '-' };
503if (!kesifHukmu.ok) {
504 await Actor.setValue('RUN_STATS', { mode, ...counters, kesifHukmu, egress });
505 await Actor.fail(`KESIF BASLAYAMADI: ${kesifHukmu.reason}. Sehir/kategori sonucu degil, ag/engel sorunu.`);
506}
507if (kesifHukmu.partial) log.warning(`KESIF EKSIK: ${kesifHukmu.reason}`);
508
509let shortfall = null;
510if (counters.pushed < maxItems) {
511 shortfall = mode === 'discovery'
512 ? shortfallMessage({
513 requested: maxItems,
514 found: counters.pushed,
515 listPagesScanned: counters.listPages,
516 listPagesAvailable: disc.matched.length,
517 city: disc.resolved?.base ?? disc.citySlug,
518 category: category ? slugify(category) : null,
519 dateFiltered: counters.dateFiltered,
520 pastFiltered: counters.skippedPast,
521 hardCapHit,
522 blocked: blocks.stats().totalBlocks ?? 0,
523 })
524 : `${maxItems} istendi, ${counters.pushed} bulundu; ${finalSeeds.length} tohum, ${counters.listPages} liste sayfasi, ${counters.dBlocked} /d/ engeli, ${counters.skippedPast} gecmis, ${counters.dateFiltered} tarih disi.`;
525 log.warning(`maxItems KARSILANAMADI: ${shortfall}`);
526}
527
528const stats = {
529 mode,
530 ...counters,
531 ...blocks.stats(),
532 semaAlanSayisi: Object.keys(EVENT_SCHEMA.fields).length,
533 isAtHome,
534 egress,
535 proxy: proxyCheck,
536 ucretlendirme: chargeCheck,
537 semaIhlalleri: [...schemaViolations],
538 muhasebe: reconcileCounters(counters),
539 kesif: mode === 'discovery' ? {
540 sehir: disc.resolved?.base ?? null,
541 slugSayisi: disc.resolved?.slugs?.length ?? 0,
542 eslesenListe: disc.matched.length,
543 taranan: counters.listPages,
544 } : null,
545 butce: budget,
546 eksik: shortfall,
547};
548log.info(`Bitti: ${JSON.stringify(stats)}`);
549log.info(`PROXY: ${proxyCheck.verdict} -- ${proxyCheck.detail}`);
550log.info(`UCRETLENDIRME: ${chargeCheck.verdict} -- ${chargeCheck.detail}`);
551log.info(`BUTCE: plan ${plan.total} istek, gercek ${actualRequests}; kayit basina plan ${budget.planPerRecord}, gercek ${budget.actualPerRecord ?? '-'}${hardCapHit ? ' [TAVAN DOLDU]' : ''}`);
552if (counters.forbidden > 0) log.error('DIKKAT: yasakli alan sayaci sifir degil, beyaz liste gozden gecirilmeli.');
553if (isAtHome && !proxyCheck.ok) log.error(`DIKKAT: ${proxyCheck.detail}`);
554if (isAtHome && !chargeCheck.ok) log.error(`DIKKAT: ${chargeCheck.detail}`);
555await Actor.setValue('RUN_STATS', stats);
556
557
558if (counters.schemaFail > 0) {
559 await Actor.fail(
560 `SEMA UYUSMAZLIGI: ${counters.schemaFail} kayit datasete yazilamadi. ` +
561 `Ihlaller: ${[...schemaViolations].join(' | ')}. ` +
562 '.actor/dataset_schema.json ile parser ciktisi ayristi.',
563 );
564}
565if (counters.pushFail > 0) {
566 await Actor.fail(`DATASET YAZMA HATASI: ${counters.pushFail} kayit yazilamadi.`);
567}
568
569
570const engelVar = (blocks.stats().totalBlocks ?? 0) > 0 || counters.parseFail > 0;
571if (counters.pushed === 0 && engelVar) {
572 await Actor.fail(
573 `Hic kayit yazilamadi; ${blocks.stats().totalBlocks} engel, ${counters.parseFail} ayristirma hatasi. ` +
574 'Datacenter IP adresi Eventbrite tarafindan dogrulamaya dusuruyor olabilir.',
575 );
576}
577
578const muhasebe = reconcileCounters(counters);
579if (!muhasebe.ok) {
580 await Actor.fail(`SAYAC TUTMUYOR: ${muhasebe.detail}. Sayilmayan bir dal var; sessiz kayip.`);
581}
582
583await Actor.exit();