build: initialize RetroHub with Laravel, Inertia, Vue, and Horizon
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features\Scraper\Services;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
interface CoverScraperInterface
|
||||
{
|
||||
public function search(string $query, ?int $consoleId = null): Collection;
|
||||
|
||||
public function getCoverUrl(int $externalId): ?string;
|
||||
|
||||
public function getScreenshotUrl(int $externalId): ?string;
|
||||
|
||||
public function getImages(int $externalId): array;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features\Scraper\Services;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class IGDBCoverScraper implements CoverScraperInterface
|
||||
{
|
||||
private string $clientId;
|
||||
|
||||
private string $clientSecret;
|
||||
|
||||
private ?string $accessToken = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->clientId = config('services.igdb.client_id', '');
|
||||
$this->clientSecret = config('services.igdb.client_secret', '');
|
||||
}
|
||||
|
||||
public function search(string $query, ?int $consoleId = null): Collection
|
||||
{
|
||||
if (empty($this->clientId) || empty($this->clientSecret)) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$body = "search \"{$query}\"; fields name,cover.url,platforms,release_dates.year;";
|
||||
if ($consoleId) {
|
||||
$body .= " where platforms = [{$consoleId}];";
|
||||
}
|
||||
|
||||
$response = $this->makeRequest('/games', $body);
|
||||
|
||||
return collect($response)->map(fn ($game) => [
|
||||
'id' => $game['id'],
|
||||
'title' => $game['name'],
|
||||
'cover_url' => isset($game['cover']['url'])
|
||||
? str_replace('thumb', 'cover_big', $game['cover']['url'])
|
||||
: null,
|
||||
'year' => $game['release_dates'][0]['year'] ?? null,
|
||||
'source' => 'igdb',
|
||||
]);
|
||||
}
|
||||
|
||||
public function getCoverUrl(int $externalId): ?string
|
||||
{
|
||||
if (empty($this->clientId) || empty($this->clientSecret)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$response = $this->makeRequest(
|
||||
'/covers',
|
||||
"fields url; where game = {$externalId};"
|
||||
);
|
||||
|
||||
if (! empty($response[0]['url'])) {
|
||||
return str_replace('thumb', 'cover_big', $response[0]['url']);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getScreenshotUrl(int $externalId): ?string
|
||||
{
|
||||
if (empty($this->clientId) || empty($this->clientSecret)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$response = $this->makeRequest(
|
||||
'/screenshots',
|
||||
"fields url; where game = {$externalId};"
|
||||
);
|
||||
|
||||
return $response[0]['url'] ?? null;
|
||||
}
|
||||
|
||||
public function getImages(int $externalId): array
|
||||
{
|
||||
return array_filter([
|
||||
'cover' => $this->getCoverUrl($externalId),
|
||||
'screenshot' => $this->getScreenshotUrl($externalId),
|
||||
]);
|
||||
}
|
||||
|
||||
private function makeRequest(string $endpoint, string $body): array
|
||||
{
|
||||
try {
|
||||
$this->ensureAccessToken();
|
||||
|
||||
$response = Http::withHeaders([
|
||||
'Client-ID' => $this->clientId,
|
||||
'Authorization' => "Bearer {$this->accessToken}",
|
||||
])->timeout(10)->post("https://api.igdb.com/v4{$endpoint}", $body);
|
||||
|
||||
if ($response->successful()) {
|
||||
return $response->json();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Log error
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function ensureAccessToken(): void
|
||||
{
|
||||
if ($this->accessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
$response = Http::asForm()->post('https://id.twitch.tv/oauth2/token', [
|
||||
'client_id' => $this->clientId,
|
||||
'client_secret' => $this->clientSecret,
|
||||
'grant_type' => 'client_credentials',
|
||||
]);
|
||||
|
||||
$this->accessToken = $response->json('access_token');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features\Scraper\Services;
|
||||
|
||||
use App\Features\Consoles\Models\Console;
|
||||
use App\Features\Games\Models\Game;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ScraperService
|
||||
{
|
||||
public function __construct(
|
||||
private IGDBCoverScraper $igdb,
|
||||
private TGDBCoverScraper $tgdb
|
||||
) {}
|
||||
|
||||
public function search(string $query, ?string $consoleSlug = null): Collection
|
||||
{
|
||||
$console = $consoleSlug
|
||||
? Console::where('slug', $consoleSlug)->first()
|
||||
: null;
|
||||
|
||||
$consoleId = $console?->igdb_platform_id;
|
||||
|
||||
$results = $this->igdb->search($query, $consoleId);
|
||||
|
||||
if ($results->isEmpty()) {
|
||||
$tgdbConsoleId = $console?->tgdb_platform_id;
|
||||
$results = $this->tgdb->search($query, $tgdbConsoleId);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function scrapeForGame(Game $game): ?string
|
||||
{
|
||||
$coverUrl = null;
|
||||
|
||||
if ($game->igdb_id) {
|
||||
$coverUrl = $this->igdb->getCoverUrl($game->igdb_id);
|
||||
}
|
||||
|
||||
if (! $coverUrl && $game->tgdb_id) {
|
||||
$coverUrl = $this->tgdb->getCoverUrl($game->tgdb_id);
|
||||
}
|
||||
|
||||
if ($coverUrl) {
|
||||
return $this->downloadCover($coverUrl, $game);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function downloadCover(string $url, Game $game): string
|
||||
{
|
||||
$consoleSlug = $game->console->slug ?? 'unknown';
|
||||
$filename = "{$game->slug}-cover.jpg";
|
||||
$path = "covers/{$consoleSlug}/{$filename}";
|
||||
|
||||
try {
|
||||
$contents = Http::timeout(30)->get($url)->body();
|
||||
Storage::disk('local')->put($path, $contents);
|
||||
|
||||
return $path;
|
||||
} catch (\Exception $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Features\Scraper\Services;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class TGDBCoverScraper implements CoverScraperInterface
|
||||
{
|
||||
private string $apiKey;
|
||||
|
||||
private string $baseUrl = 'https://api.thegamesdb.net/v1';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->apiKey = config('services.thegamesdb.api_key', '');
|
||||
}
|
||||
|
||||
public function search(string $query, ?int $consoleId = null): Collection
|
||||
{
|
||||
if (empty($this->apiKey)) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$params = [
|
||||
'apikey' => $this->apiKey,
|
||||
'name' => $query,
|
||||
];
|
||||
if ($consoleId) {
|
||||
$params['platform'] = $consoleId;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::timeout(10)->get("{$this->baseUrl}/Games/ByGameName", $params);
|
||||
|
||||
if ($response->successful()) {
|
||||
$games = $response->json('data.games') ?? [];
|
||||
|
||||
return collect($games)->map(fn ($game) => [
|
||||
'id' => $game['id'],
|
||||
'title' => $game['game_title'],
|
||||
'cover_url' => $game['cover'] ?? null,
|
||||
'year' => isset($game['release_date'])
|
||||
? date('Y', strtotime($game['release_date']))
|
||||
: null,
|
||||
'source' => 'thegamesdb',
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Log error
|
||||
}
|
||||
|
||||
return collect();
|
||||
}
|
||||
|
||||
public function getCoverUrl(int $externalId): ?string
|
||||
{
|
||||
if (empty($this->apiKey)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::timeout(10)->get("{$this->baseUrl}/Games/Images", [
|
||||
'apikey' => $this->apiKey,
|
||||
'id' => $externalId,
|
||||
'images_type' => 'boxart',
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$images = $response->json('data.images') ?? [];
|
||||
if (! empty($images[0]['filename'])) {
|
||||
return "https://cdn.thegamesdb.net/images/original/{$images[0]['filename']}";
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Log error
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getScreenshotUrl(int $externalId): ?string
|
||||
{
|
||||
if (empty($this->apiKey)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::timeout(10)->get("{$this->baseUrl}/Games/Images", [
|
||||
'apikey' => $this->apiKey,
|
||||
'id' => $externalId,
|
||||
'images_type' => 'screenshot',
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$images = $response->json('data.images') ?? [];
|
||||
if (! empty($images[0]['filename'])) {
|
||||
return "https://cdn.thegamesdb.net/images/original/{$images[0]['filename']}";
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Log error
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getImages(int $externalId): array
|
||||
{
|
||||
return array_filter([
|
||||
'cover' => $this->getCoverUrl($externalId),
|
||||
'screenshot' => $this->getScreenshotUrl($externalId),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user