GG.deals clone: CodeIgniter 4 + MongoDB game price tracker
- Full gg.deals-style UI: deal feed with filters, game pages with price history charts, rankings, vouchers, news, prepaids, subscriptions, bundles, franchises, wishlist, API docs - Real data crawlers: * gg:crawl (Steam Store API + GOG Catalog + Epic free games) * gg:catalog (252K game slugs from gg.deals sitemaps) * gg:ggdeals (per-game prices via FlareSolverr session, JSON-LD parsing) * gg:seed / gg:crawl-covers (demo seed + Steam CDN cover art) - 24/7 self-updating scheduler (scripts/scheduler_loop.php, lockfile-protected) - Tailwind v4 dark/light theme, Alpine.js, Chart.js
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Crash-safe recurring crawler scheduler with a single-instance lockfile.
|
||||
*
|
||||
* Loop:
|
||||
* 1. Acquire lockfile (fail fast if another instance is running)
|
||||
* 2. Refresh Steam + GOG + Epic prices (gg:crawl)
|
||||
* 3. Finalize aggregates
|
||||
* 4. Expand catalog: crawl next batch of N games from gg.deals via FlareSolverr
|
||||
* 5. Release lockfile, sleep interval
|
||||
*
|
||||
* Usage: php scripts/scheduler_loop.php [interval_minutes] [batch_size]
|
||||
* Example: php scripts/scheduler_loop.php 60 200
|
||||
*/
|
||||
|
||||
$intervalMin = isset($argv[1]) ? max(5, (int) $argv[1]) : 60;
|
||||
$batchSize = isset($argv[2]) ? max(10, (int) $argv[2]) : 150;
|
||||
$root = dirname(__DIR__);
|
||||
$lockFile = $root . '/writable/logs/scheduler.lock';
|
||||
$logFile = $root . '/writable/logs/scheduler.log';
|
||||
|
||||
function logMsg(string $file, string $msg): void
|
||||
{
|
||||
$line = '[' . date('Y-m-d H:i:s') . '] ' . $msg . PHP_EOL;
|
||||
file_put_contents($file, $line, FILE_APPEND);
|
||||
echo $line;
|
||||
}
|
||||
|
||||
function acquireLock(string $lockFile): bool
|
||||
{
|
||||
if (file_exists($lockFile)) {
|
||||
$pid = (int) trim((string) file_get_contents($lockFile));
|
||||
if ($pid > 0 && posix_kill($pid, 0)) {
|
||||
return false; // another instance alive
|
||||
}
|
||||
@unlink($lockFile); // stale lock
|
||||
}
|
||||
file_put_contents($lockFile, (string) getmypid());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function releaseLock(string $lockFile): void
|
||||
{
|
||||
@unlink($lockFile);
|
||||
}
|
||||
|
||||
function run(string $root, string $cmd, string $logFile, string $tag): int
|
||||
{
|
||||
logMsg($logFile, ">>> [$tag] $cmd");
|
||||
$output = [];
|
||||
$code = 0;
|
||||
exec('cd ' . escapeshellarg($root) . ' && ' . $cmd . ' 2>&1', $output, $code);
|
||||
foreach (array_slice($output, -6) as $line) {
|
||||
logMsg($logFile, " $line");
|
||||
}
|
||||
logMsg($logFile, "<<< [$tag] exit $code");
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
logMsg($logFile, "Scheduler loop started — interval {$intervalMin}min, batch {$batchSize}");
|
||||
|
||||
while (true) {
|
||||
if (! acquireLock($lockFile)) {
|
||||
logMsg($logFile, 'Another crawler instance is running — exiting this loop.');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. refresh prices from direct sources
|
||||
run($root, 'php spark gg:crawl steam', $logFile, 'steam');
|
||||
run($root, 'php spark gg:crawl gog', $logFile, 'gog');
|
||||
run($root, 'php spark gg:crawl epic', $logFile, 'epic');
|
||||
run($root, 'php spark gg:crawl finalize', $logFile, 'finalize');
|
||||
|
||||
// 2. expand catalog via FlareSolverr session
|
||||
run($root, "php spark gg:ggdeals --batch={$batchSize}", $logFile, 'ggdeals-expand');
|
||||
} catch (\Throwable $e) {
|
||||
logMsg($logFile, 'ERROR in loop: ' . $e->getMessage());
|
||||
} finally {
|
||||
releaseLock($lockFile);
|
||||
}
|
||||
|
||||
logMsg($logFile, "Sleeping {$intervalMin}min until next cycle…");
|
||||
sleep($intervalMin * 60);
|
||||
}
|
||||
Reference in New Issue
Block a user