backfill command and minor fixes
This commit is contained in:
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Console\Commands\Concerns\UpsertsGames;
|
||||||
|
use App\Services\IgdbService;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
class BackfillGamesCommand extends Command
|
||||||
|
{
|
||||||
|
use UpsertsGames;
|
||||||
|
|
||||||
|
protected $signature = 'games:backfill
|
||||||
|
{--start= : Override the starting offset (ignores saved progress)}
|
||||||
|
{--total=50000 : Stop after importing this many games total}
|
||||||
|
{--batch=500 : Games per IGDB request (max 500)}
|
||||||
|
{--sleep=2 : Seconds to sleep between batches}
|
||||||
|
{--reset : Clear saved progress and start from 0}
|
||||||
|
{--min-ratings=5 : Minimum rating_count to include}';
|
||||||
|
|
||||||
|
protected $description = 'Bulk-import games from IGDB, resuming from last saved offset if interrupted';
|
||||||
|
|
||||||
|
private const PROGRESS_KEY = 'games_backfill_offset';
|
||||||
|
|
||||||
|
public function handle(IgdbService $igdb): int
|
||||||
|
{
|
||||||
|
$batch = min((int) $this->option('batch'), 500);
|
||||||
|
$total = (int) $this->option('total');
|
||||||
|
$sleep = (int) $this->option('sleep');
|
||||||
|
$minRatings = (int) $this->option('min-ratings');
|
||||||
|
|
||||||
|
if ($this->option('reset')) {
|
||||||
|
Cache::forget(self::PROGRESS_KEY);
|
||||||
|
$this->info('Progress reset.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$startOption = $this->option('start');
|
||||||
|
$offset = $startOption !== null
|
||||||
|
? (int) $startOption
|
||||||
|
: (int) Cache::get(self::PROGRESS_KEY, 0);
|
||||||
|
|
||||||
|
$this->info("Starting backfill — offset: {$offset}, target: {$total} games, batch: {$batch}, sleep: {$sleep}s");
|
||||||
|
$this->newLine();
|
||||||
|
|
||||||
|
$imported = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
$batchNum = 0;
|
||||||
|
|
||||||
|
while ($imported < $total) {
|
||||||
|
$batchNum++;
|
||||||
|
$limit = min($batch, $total - $imported);
|
||||||
|
|
||||||
|
$this->line("Batch #{$batchNum} — offset {$offset}, fetching {$limit}…");
|
||||||
|
|
||||||
|
try {
|
||||||
|
$results = $igdb->getPopularGamesForBackfill($limit, $offset, $minRatings);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error('IGDB request failed: ' . $e->getMessage());
|
||||||
|
$this->warn("Progress saved at offset {$offset}. Re-run to resume.");
|
||||||
|
Cache::put(self::PROGRESS_KEY, $offset);
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($results)) {
|
||||||
|
$this->info('IGDB returned no more results. Backfill complete.');
|
||||||
|
Cache::forget(self::PROGRESS_KEY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$batchImported = 0;
|
||||||
|
foreach ($results as $data) {
|
||||||
|
try {
|
||||||
|
$this->upsertGame($data, $igdb);
|
||||||
|
$batchImported++;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$skipped++;
|
||||||
|
$this->warn(" Skipped igdb_id={$data['id']}: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$imported += $batchImported;
|
||||||
|
$offset += count($results);
|
||||||
|
|
||||||
|
Cache::put(self::PROGRESS_KEY, $offset);
|
||||||
|
|
||||||
|
$this->line(" → {$batchImported} upserted | total so far: {$imported} | next offset: {$offset}");
|
||||||
|
|
||||||
|
if ($imported < $total && count($results) === $limit) {
|
||||||
|
sleep($sleep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("Backfill finished. Imported/updated: {$imported}, skipped: {$skipped}.");
|
||||||
|
|
||||||
|
if ($imported >= $total) {
|
||||||
|
$this->comment("Reached --total={$total}. Run again (or with a higher --total) to continue from offset {$offset}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands\Concerns;
|
||||||
|
|
||||||
|
use App\Models\Company;
|
||||||
|
use App\Models\Game;
|
||||||
|
use App\Models\GameScreenshot;
|
||||||
|
use App\Models\Genre;
|
||||||
|
use App\Models\Platform;
|
||||||
|
use App\Services\IgdbService;
|
||||||
|
|
||||||
|
trait UpsertsGames
|
||||||
|
{
|
||||||
|
protected function upsertGame(array $data, IgdbService $igdb): void
|
||||||
|
{
|
||||||
|
$igdbId = $data['id'];
|
||||||
|
$title = $data['name'] ?? 'Unknown';
|
||||||
|
$releaseDate = isset($data['first_release_date'])
|
||||||
|
? date('Y-m-d', $data['first_release_date'])
|
||||||
|
: null;
|
||||||
|
$releaseYear = $releaseDate ? substr($releaseDate, 0, 4) : null;
|
||||||
|
|
||||||
|
$coverUrl = null;
|
||||||
|
$coverThumbUrl = null;
|
||||||
|
if (!empty($data['cover']['image_id'])) {
|
||||||
|
$coverUrl = $igdb::coverUrl($data['cover']['image_id'], 'cover_big');
|
||||||
|
$coverThumbUrl = $igdb::coverUrl($data['cover']['image_id'], 'cover_small');
|
||||||
|
}
|
||||||
|
|
||||||
|
$game = Game::updateOrCreate(
|
||||||
|
['igdb_id' => $igdbId],
|
||||||
|
[
|
||||||
|
'title' => $title,
|
||||||
|
'summary' => $data['summary'] ?? null,
|
||||||
|
'cover_url' => $coverUrl,
|
||||||
|
'cover_thumb_url' => $coverThumbUrl,
|
||||||
|
'release_date' => $releaseDate,
|
||||||
|
'release_year' => $releaseYear,
|
||||||
|
'status' => 'published',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!empty($data['genres'])) {
|
||||||
|
$genreIds = [];
|
||||||
|
foreach ($data['genres'] as $g) {
|
||||||
|
$genre = Genre::firstOrCreate(
|
||||||
|
['igdb_id' => $g['id']],
|
||||||
|
['name' => $g['name'], 'slug' => \Str::slug($g['name'])]
|
||||||
|
);
|
||||||
|
$genreIds[] = $genre->id;
|
||||||
|
}
|
||||||
|
$game->genres()->sync($genreIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['platforms'])) {
|
||||||
|
$platformIds = [];
|
||||||
|
foreach ($data['platforms'] as $p) {
|
||||||
|
$platform = Platform::firstOrCreate(
|
||||||
|
['igdb_id' => $p['id']],
|
||||||
|
['name' => $p['name'], 'slug' => \Str::slug($p['name'])]
|
||||||
|
);
|
||||||
|
$platformIds[] = $platform->id;
|
||||||
|
}
|
||||||
|
$game->platforms()->sync($platformIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['involved_companies'])) {
|
||||||
|
$companyPivot = [];
|
||||||
|
foreach ($data['involved_companies'] as $ic) {
|
||||||
|
if (empty($ic['company']['name'])) continue;
|
||||||
|
$company = Company::firstOrCreate(
|
||||||
|
['igdb_id' => $ic['company']['id']],
|
||||||
|
['name' => $ic['company']['name'], 'slug' => \Str::slug($ic['company']['name'])]
|
||||||
|
);
|
||||||
|
$role = $ic['developer'] ? 'developer' : ($ic['publisher'] ? 'publisher' : 'other');
|
||||||
|
$companyPivot[$company->id] = ['role' => $role];
|
||||||
|
}
|
||||||
|
$game->companies()->sync($companyPivot);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($data['screenshots'])) {
|
||||||
|
GameScreenshot::where('game_id', $game->id)->delete();
|
||||||
|
foreach ($data['screenshots'] as $i => $shot) {
|
||||||
|
GameScreenshot::create([
|
||||||
|
'game_id' => $game->id,
|
||||||
|
'url' => $igdb::screenshotUrl($shot['image_id'], 'screenshot_big'),
|
||||||
|
'thumb_url' => $igdb::screenshotUrl($shot['image_id'], 'screenshot_med'),
|
||||||
|
'order' => $i,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,11 +33,11 @@ class GenerateSitemapCommand extends Command
|
|||||||
});
|
});
|
||||||
|
|
||||||
User::where('is_private', false)
|
User::where('is_private', false)
|
||||||
->select('username', 'updated_at')
|
->select('slug', 'updated_at')
|
||||||
->chunk(500, function ($users) use ($sitemap) {
|
->chunk(500, function ($users) use ($sitemap) {
|
||||||
foreach ($users as $user) {
|
foreach ($users as $user) {
|
||||||
$sitemap->add(
|
$sitemap->add(
|
||||||
Url::create("/users/{$user->username}")
|
Url::create("/users/{$user->slug}")
|
||||||
->setLastModificationDate($user->updated_at)
|
->setLastModificationDate($user->updated_at)
|
||||||
->setPriority(0.5)
|
->setPriority(0.5)
|
||||||
->setChangeFrequency(Url::CHANGE_FREQUENCY_WEEKLY)
|
->setChangeFrequency(Url::CHANGE_FREQUENCY_WEEKLY)
|
||||||
|
|||||||
@@ -2,16 +2,13 @@
|
|||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Models\Company;
|
use App\Console\Commands\Concerns\UpsertsGames;
|
||||||
use App\Models\Game;
|
|
||||||
use App\Models\GameScreenshot;
|
|
||||||
use App\Models\Genre;
|
|
||||||
use App\Models\Platform;
|
|
||||||
use App\Services\IgdbService;
|
use App\Services\IgdbService;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
class ImportGamesCommand extends Command
|
class ImportGamesCommand extends Command
|
||||||
{
|
{
|
||||||
|
use UpsertsGames;
|
||||||
protected $signature = 'games:import {--limit=100} {--offset=0} {--popular} {--new} {--year=} {--platform=}';
|
protected $signature = 'games:import {--limit=100} {--offset=0} {--popular} {--new} {--year=} {--platform=}';
|
||||||
protected $description = 'Import games from IGDB into the local database';
|
protected $description = 'Import games from IGDB into the local database';
|
||||||
|
|
||||||
@@ -74,87 +71,4 @@ class ImportGamesCommand extends Command
|
|||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function upsertGame(array $data, IgdbService $igdb): void
|
|
||||||
{
|
|
||||||
$igdbId = $data['id'];
|
|
||||||
$title = $data['name'] ?? 'Unknown';
|
|
||||||
$releaseDate = isset($data['first_release_date'])
|
|
||||||
? date('Y-m-d', $data['first_release_date'])
|
|
||||||
: null;
|
|
||||||
$releaseYear = $releaseDate ? substr($releaseDate, 0, 4) : null;
|
|
||||||
|
|
||||||
$coverUrl = null;
|
|
||||||
$coverThumbUrl = null;
|
|
||||||
if (!empty($data['cover']['image_id'])) {
|
|
||||||
$coverUrl = $igdb::coverUrl($data['cover']['image_id'], 'cover_big');
|
|
||||||
$coverThumbUrl = $igdb::coverUrl($data['cover']['image_id'], 'cover_small');
|
|
||||||
}
|
|
||||||
|
|
||||||
$game = Game::updateOrCreate(
|
|
||||||
['igdb_id' => $igdbId],
|
|
||||||
[
|
|
||||||
'title' => $title,
|
|
||||||
'summary' => $data['summary'] ?? null,
|
|
||||||
'cover_url' => $coverUrl,
|
|
||||||
'cover_thumb_url'=> $coverThumbUrl,
|
|
||||||
'release_date' => $releaseDate,
|
|
||||||
'release_year' => $releaseYear,
|
|
||||||
'status' => 'published',
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Genres
|
|
||||||
if (!empty($data['genres'])) {
|
|
||||||
$genreIds = [];
|
|
||||||
foreach ($data['genres'] as $g) {
|
|
||||||
$genre = Genre::firstOrCreate(
|
|
||||||
['igdb_id' => $g['id']],
|
|
||||||
['name' => $g['name'], 'slug' => \Str::slug($g['name'])]
|
|
||||||
);
|
|
||||||
$genreIds[] = $genre->id;
|
|
||||||
}
|
|
||||||
$game->genres()->sync($genreIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Platforms
|
|
||||||
if (!empty($data['platforms'])) {
|
|
||||||
$platformIds = [];
|
|
||||||
foreach ($data['platforms'] as $p) {
|
|
||||||
$platform = Platform::firstOrCreate(
|
|
||||||
['igdb_id' => $p['id']],
|
|
||||||
['name' => $p['name'], 'slug' => \Str::slug($p['name'])]
|
|
||||||
);
|
|
||||||
$platformIds[] = $platform->id;
|
|
||||||
}
|
|
||||||
$game->platforms()->sync($platformIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Companies (involved_companies)
|
|
||||||
if (!empty($data['involved_companies'])) {
|
|
||||||
$companyPivot = [];
|
|
||||||
foreach ($data['involved_companies'] as $ic) {
|
|
||||||
if (empty($ic['company']['name'])) continue;
|
|
||||||
$company = Company::firstOrCreate(
|
|
||||||
['igdb_id' => $ic['company']['id']],
|
|
||||||
['name' => $ic['company']['name'], 'slug' => \Str::slug($ic['company']['name'])]
|
|
||||||
);
|
|
||||||
$role = $ic['developer'] ? 'developer' : ($ic['publisher'] ? 'publisher' : 'other');
|
|
||||||
$companyPivot[$company->id] = ['role' => $role];
|
|
||||||
}
|
|
||||||
$game->companies()->sync($companyPivot);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Screenshots
|
|
||||||
if (!empty($data['screenshots'])) {
|
|
||||||
GameScreenshot::where('game_id', $game->id)->delete();
|
|
||||||
foreach ($data['screenshots'] as $i => $shot) {
|
|
||||||
GameScreenshot::create([
|
|
||||||
'game_id' => $game->id,
|
|
||||||
'url' => $igdb::screenshotUrl($shot['image_id'], 'screenshot_big'),
|
|
||||||
'thumb_url' => $igdb::screenshotUrl($shot['image_id'], 'screenshot_med'),
|
|
||||||
'order' => $i,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,27 @@ class IgdbService
|
|||||||
");
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get games for backfill — broader filter than getPopularGames.
|
||||||
|
*/
|
||||||
|
public function getPopularGamesForBackfill(int $limit = 500, int $offset = 0, int $minRatings = 5): array
|
||||||
|
{
|
||||||
|
return $this->query('games', "
|
||||||
|
fields id,name,slug,summary,cover.image_id,first_release_date,
|
||||||
|
genres.id,genres.name,genres.slug,
|
||||||
|
platforms.id,platforms.name,platforms.slug,platforms.abbreviation,
|
||||||
|
involved_companies.company.id,involved_companies.company.name,
|
||||||
|
involved_companies.company.slug,involved_companies.developer,
|
||||||
|
involved_companies.publisher,
|
||||||
|
screenshots.image_id,
|
||||||
|
rating,rating_count;
|
||||||
|
where rating != null & rating_count > {$minRatings} & cover != null;
|
||||||
|
sort rating_count desc;
|
||||||
|
limit {$limit};
|
||||||
|
offset {$offset};
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get recently released games.
|
* Get recently released games.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user