lots of progress

This commit is contained in:
2025-04-12 17:18:33 +03:00
parent 8d230e8e67
commit 3d4260d57e
44 changed files with 2590 additions and 2883 deletions
+2 -6
View File
@@ -40,12 +40,7 @@ class BuildGameLibrary extends Command
$startTimestamp = strtotime("{$year}-01-01");
$endTimestamp = strtotime("{$year}-12-31");
// Generate a random query to get diverse results
$query = $this->generateRandomQuery();
$this->info("Searching for games from {$year} with query: {$query}");
$games = $igdbService->get();
$games = $igdbService->get($limit);
if (empty($games)) {
$this->warn("No games found for this batch. Moving to next batch...");
@@ -66,6 +61,7 @@ class BuildGameLibrary extends Command
$totalAdded++;
} catch (\Exception $e) {
$this->warn("\nFailed to process game: {$e->getMessage()}");
dd($e);
$totalSkipped++;
}
+4 -2
View File
@@ -7,6 +7,7 @@ use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use JetBrains\PhpStorm\NoReturn;
use ZipArchive;
class FetchIgdbDumps extends Command
@@ -68,6 +69,7 @@ class FetchIgdbDumps extends Command
return Command::SUCCESS;
}
#[NoReturn]
private function fetchDumps(string $type, int $limit, int $offset, string $outputDir, IgdbAuthService $authService): void
{
$endpoint = self::ENDPOINTS[$type];
@@ -81,13 +83,13 @@ class FetchIgdbDumps extends Command
$accessToken = $authService->getAccessToken();
$response = Http::withHeaders([
'Client-ID' => config('services.igdb.credentials.client_id'),
'Client-ID' => config('igdb.credentials.client_id'),
'Authorization' => 'Bearer ' . $accessToken,
])->post($endpoint, [
'fields' => $fields,
'limit' => $limit,
'offset' => $offset,
'where' => 'rating_count > 0',
// 'where' => 'rating_count > 0',
]);
if (!$response->successful()) {
+1 -1
View File
@@ -2,7 +2,7 @@
namespace App\Http\Controllers;
abstract class Controller
abstract class Controller extends \Illuminate\Routing\Controller
{
//
}
+76 -1
View File
@@ -29,4 +29,79 @@ class GameController extends Controller
return response()->json($game, 201);
}
}
public function index(Request $request)
{
$query = Game::query();
// Apply search
if ($request->has('search')) {
$search = $request->get('search');
$query->where(function($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('summary', 'like', "%{$search}%");
});
}
// Apply sorting
$sort = $request->get('sort', 'first_release_date');
$direction = $request->get('direction', 'desc');
if ($sort === 'rating') {
$query->orderBy('rating', $direction);
} elseif ($sort === 'rating_count') {
$query->orderBy('rating_count', $direction);
} else {
$query->orderBy('first_release_date', $direction);
}
// Apply filters
if ($request->has('platform')) {
$query->whereHas('platforms', function ($q) use ($request) {
$q->where('slug', $request->platform);
});
}
if ($request->has('genre')) {
$query->whereHas('genres', function ($q) use ($request) {
$q->where('slug', $request->genre);
});
}
if ($request->has('year')) {
$query->whereYear('first_release_date', $request->year);
}
if ($request->has('rating_range')) {
$range = explode('-', $request->rating_range);
if (count($range) === 2) {
$min = (float)$range[0];
$max = (float)$range[1];
$query->whereBetween('rating', [$min, $max]);
}
}
$games = $query->paginate(12)->withQueryString();
// Get data for filter dropdowns
$platforms = \App\Models\Platform::orderBy('name')->get();
$genres = \App\Models\Genre::orderBy('name')->get();
// Get unique years from games
$years = Game::selectRaw('YEAR(first_release_date) as year')
->whereNotNull('first_release_date')
->distinct()
->orderBy('year', 'desc')
->pluck('year')
->filter()
->values();
return view('games.index', compact('games', 'platforms', 'genres', 'years'));
}
public function show(Game $game)
{
$game->load(['platforms', 'genres', 'screenshots', 'videos', 'similar_games', 'dlcs', 'reviews.user']);
return view('games.show', compact('game'));
}
}
+128
View File
@@ -0,0 +1,128 @@
<?php
namespace App\Http\Controllers;
use App\Models\Game;
use App\Models\GameList;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class GameListController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
/**
* Display a listing of the resource.
*/
public function index()
{
$lists = Auth::user()->lists()->withCount('games')->get();
return view('lists.index', compact('lists'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
return view('lists.create');
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string'
]);
Auth::user()->lists()->create([
'name' => $request->name,
'description' => $request->description
]);
return redirect()->route('lists.index')->with('success', 'List created successfully.');
}
/**
* Display the specified resource.
*/
public function show(GameList $list)
{
$this->authorize('view', $list);
$games = $list->games()->paginate(12);
return view('lists.show', compact('list', 'games'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(GameList $list)
{
$this->authorize('update', $list);
return view('lists.edit', compact('list'));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, GameList $list)
{
$this->authorize('update', $list);
$request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string'
]);
$list->update([
'name' => $request->name,
'description' => $request->description
]);
return redirect()->route('lists.show', $list)->with('success', 'List updated successfully.');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(GameList $list)
{
$this->authorize('delete', $list);
if ($list->is_default) {
return redirect()->back()->with('error', 'Cannot delete default lists.');
}
$list->delete();
return redirect()->route('lists.index')->with('success', 'List deleted successfully.');
}
public function addGame(Request $request, GameList $list)
{
$this->authorize('update', $list);
$request->validate([
'game_id' => 'required|exists:games,id'
]);
$list->games()->attach($request->game_id);
return redirect()->back()->with('success', 'Game added to list successfully.');
}
public function removeGame(GameList $list, Game $game)
{
$this->authorize('update', $list);
$list->games()->detach($game->id);
return redirect()->back()->with('success', 'Game removed from list successfully.');
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
namespace App\Http\Controllers;
use App\Models\Game;
use App\Models\Review;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ReviewController extends Controller
{
public function __construct()
{
$this->middleware('auth', ['except' => ['index', 'show']]);
}
public function index(Game $game)
{
$reviews = $game->reviews()->with('user')->latest()->paginate(10);
return view('reviews.index', compact('game', 'reviews'));
}
public function create(Game $game)
{
return view('reviews.create', compact('game'));
}
public function store(Request $request, Game $game)
{
$validated = $request->validate([
'content' => 'required|string|min:10|max:1000',
'recommended' => 'required|boolean',
'rating' => 'required|integer|between:1,10',
]);
$review = new Review($validated);
$review->user_id = Auth::id();
$review->game_id = $game->id;
$review->save();
return redirect()->route('games.show', $game->slug)
->with('success', 'Review posted successfully!');
}
public function show(Game $game, Review $review)
{
return view('reviews.show', compact('game', 'review'));
}
public function edit(Game $game, Review $review)
{
$this->authorize('update', $review);
return view('reviews.edit', compact('game', 'review'));
}
public function update(Request $request, Game $game, Review $review)
{
$this->authorize('update', $review);
$validated = $request->validate([
'content' => 'required|string|min:10|max:1000',
'recommended' => 'required|boolean',
'rating' => 'required|integer|between:1,10',
]);
$review->update($validated);
return redirect()->route('games.show', $game->slug)
->with('success', 'Review updated successfully!');
}
public function destroy(Game $game, Review $review)
{
$this->authorize('delete', $review);
$review->delete();
return redirect()->route('games.show', $game->slug)
->with('success', 'Review deleted successfully!');
}
}
+74 -3
View File
@@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Game extends Model
{
@@ -34,11 +35,81 @@ class Game extends Model
public function genres(): BelongsToMany
{
return $this->belongsToMany(Genre::class);
return $this->belongsToMany(Genre::class, 'game_genre', 'game_id', 'genre_id');
}
public function platforms(): BelongsToMany
{
return $this->belongsToMany(Platform::class);
return $this->belongsToMany(Platform::class, 'game_platform', 'game_id', 'platform_id');
}
}
public function screenshots(): HasMany
{
return $this->hasMany(Screenshot::class);
}
public function videos(): HasMany
{
return $this->hasMany(Video::class);
}
public function bundles(): BelongsToMany
{
return $this->belongsToMany(Game::class, 'game_bundles', 'game_id', 'bundle_id');
}
public function collections(): BelongsToMany
{
return $this->belongsToMany(Game::class, 'game_collections', 'game_id', 'collection_id');
}
public function dlcs(): BelongsToMany
{
return $this->belongsToMany(Game::class, 'game_dlcs', 'game_id', 'dlc_id');
}
public function expansions(): BelongsToMany
{
return $this->belongsToMany(Game::class, 'game_expansions', 'game_id', 'expansion_id');
}
public function franchises(): BelongsToMany
{
return $this->belongsToMany(Game::class, 'game_franchises', 'game_id', 'franchise_id');
}
public function ports(): BelongsToMany
{
return $this->belongsToMany(Game::class, 'game_ports', 'game_id', 'port_id');
}
public function remakes(): BelongsToMany
{
return $this->belongsToMany(Game::class, 'game_remakes', 'game_id', 'remake_id');
}
public function similar_games(): BelongsToMany
{
return $this->belongsToMany(Game::class, 'game_similar', 'game_id', 'similar_game_id');
}
public function reviews(): HasMany
{
return $this->hasMany(Review::class);
}
public function ratings()
{
return $this->hasMany(Rating::class);
}
public function lists()
{
return $this->belongsToMany(GameList::class)->withTimestamps();
}
public function averageRating()
{
return $this->ratings()->avg('rating');
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Str;
class GameList extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'name',
'slug',
'description',
'is_default'
];
protected static function boot()
{
parent::boot();
static::creating(function ($list) {
$list->slug = Str::slug($list->name);
});
static::updating(function ($list) {
$list->slug = Str::slug($list->name);
});
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function games(): BelongsToMany
{
return $this->belongsToMany(Game::class)->withTimestamps();
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Rating extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'game_id',
'rating',
];
protected $casts = [
'rating' => 'integer',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function game(): BelongsTo
{
return $this->belongsTo(Game::class);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Review extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'game_id',
'content',
'recommended',
'rating',
];
protected $casts = [
'recommended' => 'boolean',
'rating' => 'integer',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function game(): BelongsTo
{
return $this->belongsTo(Game::class);
}
}
+35 -5
View File
@@ -6,6 +6,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Relations\HasMany;
class User extends Authenticatable
{
@@ -38,11 +39,40 @@ class User extends Authenticatable
*
* @return array<string, string>
*/
protected function casts(): array
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
public function reviews(): HasMany
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
return $this->hasMany(Review::class);
}
public function ratings(): HasMany
{
return $this->hasMany(Rating::class);
}
public function lists()
{
return $this->hasMany(GameList::class);
}
protected static function boot()
{
parent::boot();
static::created(function ($user) {
// Create default lists
$defaultLists = [
['name' => 'Played Games', 'is_default' => true],
['name' => 'Want to Play', 'is_default' => true]
];
foreach ($defaultLists as $list) {
$user->lists()->create($list);
}
});
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace App\Policies;
use App\Models\GameList;
use App\Models\User;
use Illuminate\Auth\Access\Response;
use Illuminate\Auth\Access\HandlesAuthorization;
class GameListPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return false;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, GameList $list)
{
return $user->id === $list->user_id;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return false;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, GameList $list)
{
return $user->id === $list->user_id;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, GameList $list)
{
return $user->id === $list->user_id;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, GameList $gameList): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, GameList $gameList): bool
{
return false;
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
namespace App\Policies;
use App\Models\Review;
use App\Models\User;
use Illuminate\Auth\Access\Response;
use Illuminate\Auth\Access\HandlesAuthorization;
class ReviewPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return false;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Review $review): bool
{
return false;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return false;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Review $review): bool
{
return $user->id === $review->user_id;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Review $review): bool
{
return $user->id === $review->user_id;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Review $review): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Review $review): bool
{
return false;
}
}
+2 -1
View File
@@ -2,6 +2,7 @@
namespace App\Providers;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@@ -19,6 +20,6 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
//
Paginator::useBootstrap();
}
}
+22 -5
View File
@@ -9,7 +9,7 @@ use Illuminate\Support\Facades\Log;
class IgdbAuthService
{
private const TOKEN_CACHE_KEY = 'igdb_access_token';
private const TOKEN_EXPIRY = 3600; // 1 hour in seconds
private const TOKEN_EXPIRY = 3600*24*31; // 1 month in seconds
public function getAccessToken(): string
{
@@ -21,13 +21,30 @@ class IgdbAuthService
private function fetchNewAccessToken(): string
{
try {
$response = Http::post('https://id.twitch.tv/oauth2/token', [
'client_id' => config('services.igdb.client_id'),
'client_secret' => config('services.igdb.client_secret'),
// Log the config values to debug
Log::info('IGDB Auth Attempt', [
'client_id' => config('services.igdb.credentials.client_id'),
'client_secret' => config('services.igdb.credentials.client_secret')
]);
$clientId = config('igdb.credentials.client_id');
$clientSecret = config('igdb.credentials.client_secret');
if (empty($clientId) || empty($clientSecret)) {
throw new \Exception("IGDB credentials are not properly configured");
}
$response = Http::asForm()->post('https://id.twitch.tv/oauth2/token', [
'client_id' => $clientId,
'client_secret' => $clientSecret,
'grant_type' => 'client_credentials',
]);
if (!$response->successful()) {
Log::error('IGDB Auth Response', [
'status' => $response->status(),
'body' => $response->body()
]);
throw new \Exception("Failed to get access token: " . $response->body());
}
@@ -40,4 +57,4 @@ class IgdbAuthService
throw $e;
}
}
}
}
+100 -34
View File
@@ -17,7 +17,7 @@ use MarcReichel\IGDBLaravel\Models\Platform as IgdbPlatform;
class IgdbService
{
public function get()
public function get(int $limit)
{
try {
$existingIds = $this->getExistingIds();
@@ -26,7 +26,7 @@ class IgdbService
'id',
'name',
'summary',
'cover.url',
'cover',
'slug',
'genres',
'platforms',
@@ -34,17 +34,21 @@ class IgdbService
'rating',
'rating_count'
])
->where('rating_count', '>', 0)
->where('category', '=', 0);
// ->where('rating_count', '>', 0)
// ->where('category', '=', 0)
;
if (!empty($existingIds)) {
$queryBuilder = $queryBuilder->whereNotIn('id', array_map(function($id) {
return (int)str_replace('"', '', $id);
}, $existingIds));
// Convert array of quoted strings to array of integers
// $existingIds = array_map(function($id) {
// return (int)str_replace('"', '', $id);
// }, $existingIds);
$queryBuilder->whereNotIn('id', $existingIds);
}
$a = $queryBuilder
->limit(50)
->orderBy('first_release_date', 'asc')
->limit($limit)
->get();
}catch (\Throwable $e) {
dd(
@@ -68,12 +72,13 @@ class IgdbService
/** @var Game $existingGame */
foreach ($existingGames as $existingGame) {
$existingIds[] = (string)$existingGame->id;
$existingIds[] = (string)$existingGame->igdb_id;
}
return array_map(function($id) {
return '"'.$id.'"';
}, $existingIds);
return $existingIds;
// return array_map(function($id) {
// return '"'.$id.'"';
// }, $existingIds);
}
public function searchGames(string $query, int $limit = 10, ?int $startTimestamp = null, ?int $endTimestamp = null): array
@@ -240,13 +245,41 @@ class IgdbService
}
}
// Process genres
$genres = [];
if (isset($gameData->genres) && is_array($gameData->genres)) {
$genreData = IgdbGenre::select(['id', 'name', 'slug'])
->whereIn('id', $gameData->genres)
->get();
foreach ($genreData as $genreDetails) {
$genre = Genre::updateOrCreate(
['igdb_id' => $genreDetails->id],
[
'name' => $genreDetails->name,
'slug' => $genreDetails->slug,
]
);
$genres[] = $genre->id;
}
}
$cover = Cover::where('game', '=', $gameData->id)->first();
$coverUrl = null;
if ($cover) {
$coverUrl = 'https:'.$cover->url;
$coverUrl = str_replace('t_thumb', 't_cover_big', $coverUrl);
}
// Create or update the game
$game = Game::updateOrCreate(
['igdb_id' => $gameData->id],
[
'name' => $gameData->name,
'summary' => $gameData->summary ?? null,
'cover_url' => $gameData->cover?->url ?? null,
'cover_url' => $coverUrl,
'slug' => $gameData->slug,
'first_release_date' => $gameData->first_release_date ?? null,
'rating' => $gameData->rating ?? null,
@@ -254,12 +287,20 @@ class IgdbService
]
);
// Sync platforms and genres
if (!empty($platforms)) {
$game->platforms()->sync($platforms);
}
if (!empty($genres)) {
$game->genres()->sync($genres);
}
// Process artworks
try {
$artworks = \MarcReichel\IGDBLaravel\Models\Artwork::select([
'id',
'url',
'width',
'width',
'height',
'image_id'
])
@@ -285,11 +326,6 @@ class IgdbService
]);
}
// Process platforms relationship
if (!empty($platforms)) {
$game->platforms()->sync($platforms);
}
// Process screenshots
try {
$screenshots = \MarcReichel\IGDBLaravel\Models\Screenshot::select([
@@ -325,7 +361,7 @@ class IgdbService
try {
$videos = \MarcReichel\IGDBLaravel\Models\GameVideo::select([
'id',
'name',
'name',
'video_id',
'url',
'checksum'
@@ -365,25 +401,55 @@ class IgdbService
throw new \InvalidArgumentException('Game data must be an instance of IgdbGame');
}
// Fetch related games data using IGDB API
$relationships = [
'bundles' => IgdbGame::select(['id'])->where('id', 'in', $gameData->bundles ?? [])->get()->pluck('id')->toArray(),
'collections' => IgdbGame::select(['id'])->where('id', 'in', $gameData->collections ?? [])->get()->pluck('id')->toArray(),
'dlcs' => IgdbGame::select(['id'])->where('id', 'in', $gameData->dlcs ?? [])->get()->pluck('id')->toArray(),
'expanded_games' => IgdbGame::select(['id'])->where('id', 'in', $gameData->expanded_games ?? [])->get()->pluck('id')->toArray(),
'franchises' => IgdbGame::select(['id'])->where('id', 'in', $gameData->franchises ?? [])->get()->pluck('id')->toArray(),
'ports' => IgdbGame::select(['id'])->where('id', 'in', $gameData->ports ?? [])->get()->pluck('id')->toArray(),
'remakes' => IgdbGame::select(['id'])->where('id', 'in', $gameData->remakes ?? [])->get()->pluck('id')->toArray(),
'similar_games' => IgdbGame::select(['id'])->where('id', 'in', $gameData->similar_games ?? [])->get()->pluck('id')->toArray(),
// Initialize relationships array
$relationships = [];
// Define relationship mappings and check each one
$relationshipMappings = [
'bundles',
'collections',
'dlcs',
'expanded_games',
'franchises',
'ports',
'remakes',
'similar_games'
];
foreach ($relationships as $igdbKey => $relationship) {
// Only query IGDB for relationships that have data
foreach ($relationshipMappings as $relationship) {
if (!empty($gameData->$relationship)) {
$relationships[$relationship] = IgdbGame::select(['id'])
->where('id', 'in', $gameData->$relationship)
->get()
->pluck('id')
->toArray();
} else {
$relationships[$relationship] = [];
}
}
// Process and sync each relationship
foreach ($relationships as $igdbKey => $relatedIds) {
// Convert IGDB relationship key to Laravel relationship method
$relationshipMethod = str_replace('_', '', $igdbKey); // Remove underscores
$relatedGames = [];
if (isset($gameData->$igdbKey) && !empty($gameData->$igdbKey)) {
$relatedIds = collect($gameData->$igdbKey)->pluck('id')->toArray();
if (!empty($relatedIds)) {
$relatedGames = Game::whereIn('igdb_id', $relatedIds)->pluck('id')->toArray();
}
$game->$relationship()->sync($relatedGames);
// Only sync if the relationship method exists
if (method_exists($game, $relationshipMethod)) {
Log::info("Syncing relationship '$relationshipMethod' for game {$game->id}");
$game->$relationshipMethod()->sync($relatedGames);
} else {
Log::warning("Relationship method '$relationshipMethod' does not exist on Game model", [
'game_id' => $game->id,
'igdb_key' => $igdbKey
]);
}
}
}