153 lines
4.9 KiB
PHP
153 lines
4.9 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Models\Anime;
|
||
use Illuminate\Support\Facades\Cache;
|
||
use Illuminate\Support\Facades\Http;
|
||
use Illuminate\Support\Facades\Log;
|
||
|
||
class AniListService
|
||
{
|
||
const ENDPOINT = 'https://graphql.anilist.co';
|
||
const CACHE_TTL = 60 * 60 * 24 * 7;
|
||
|
||
// Maksimum genişlik (px) — oran korunur, WebP'ye çevrilir
|
||
const COVER_MAX_W = 600;
|
||
const BANNER_MAX_W = 1920;
|
||
const WEBP_QUALITY = 90;
|
||
|
||
private function query(array $variables, string $gql): ?array
|
||
{
|
||
try {
|
||
$res = Http::timeout(10)
|
||
->withHeaders(['Content-Type' => 'application/json', 'Accept' => 'application/json'])
|
||
->post(self::ENDPOINT, ['query' => $gql, 'variables' => $variables]);
|
||
|
||
if (!$res->ok()) return null;
|
||
if (!empty($res->json('errors'))) return null;
|
||
|
||
return $res->json('data.Media');
|
||
} catch (\Throwable $e) {
|
||
Log::debug('AniList query failed', ['err' => $e->getMessage()]);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
public function fetchByMalId(int $malId): ?array
|
||
{
|
||
return Cache::remember("anilist_mal_{$malId}", self::CACHE_TTL, function () use ($malId) {
|
||
return $this->query(
|
||
['malId' => $malId],
|
||
'query($malId:Int){Media(idMal:$malId,type:ANIME){coverImage{extraLarge}bannerImage}}'
|
||
);
|
||
});
|
||
}
|
||
|
||
public function fetchByTitle(string $title): ?array
|
||
{
|
||
return Cache::remember('anilist_title_' . md5($title), self::CACHE_TTL, function () use ($title) {
|
||
return $this->query(
|
||
['search' => $title],
|
||
'query($search:String){Media(search:$search,type:ANIME){coverImage{extraLarge}bannerImage}}'
|
||
);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Resmi indir, yeniden boyutlandır, WebP olarak storage'a kaydet.
|
||
* Başarılıysa storage-relative yolu döner (örn. anime/covers/123.webp).
|
||
*/
|
||
/**
|
||
* Resmi indir, max genişliğe orantılı küçült (asla büyütme), WebP kaydet.
|
||
* Orijinalden küçükse olduğu gibi bırakır.
|
||
*/
|
||
private function downloadAndResize(string $url, string $storagePath, int $maxW): ?string
|
||
{
|
||
try {
|
||
$response = Http::timeout(20)->withHeaders([
|
||
'User-Agent' => 'Mozilla/5.0',
|
||
'Referer' => 'https://anilist.co/',
|
||
])->get($url);
|
||
|
||
if (!$response->ok()) return null;
|
||
|
||
$raw = $response->body();
|
||
$src = @imagecreatefromstring($raw);
|
||
if (!$src) return null;
|
||
|
||
$srcW = imagesx($src);
|
||
$srcH = imagesy($src);
|
||
|
||
if ($srcW > $maxW) {
|
||
// Orantılı küçült
|
||
$newW = $maxW;
|
||
$newH = (int) round($srcH * ($maxW / $srcW));
|
||
$dst = imagecreatetruecolor($newW, $newH);
|
||
imagecopyresampled($dst, $src, 0, 0, 0, 0, $newW, $newH, $srcW, $srcH);
|
||
imagedestroy($src);
|
||
} else {
|
||
// Zaten küçük — olduğu gibi kullan
|
||
$dst = $src;
|
||
}
|
||
|
||
$absPath = storage_path('app/public/' . $storagePath);
|
||
@mkdir(dirname($absPath), 0755, true);
|
||
|
||
$ok = imagewebp($dst, $absPath, self::WEBP_QUALITY);
|
||
imagedestroy($dst);
|
||
|
||
return $ok ? $storagePath : null;
|
||
} catch (\Throwable $e) {
|
||
Log::debug('AniList image download failed', ['url' => $url, 'err' => $e->getMessage()]);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Anime'nin boş kapak/banner alanlarını AniList'ten doldur.
|
||
* Resimleri indirir, boyutlandırır, WebP olarak storage'a kaydeder.
|
||
* Dolu alanların üzerine yazmaz.
|
||
*/
|
||
public function fillImages(Anime $anime): bool
|
||
{
|
||
$needCover = empty($anime->cover_image);
|
||
$needBanner = empty($anime->banner_image);
|
||
if (!$needCover && !$needBanner) return false;
|
||
|
||
$data = null;
|
||
if ($anime->mal_id) {
|
||
$data = $this->fetchByMalId((int) $anime->mal_id);
|
||
}
|
||
if (!$data) {
|
||
$data = $this->fetchByTitle($anime->title);
|
||
}
|
||
if (!$data) return false;
|
||
|
||
$updates = [];
|
||
|
||
if ($needCover && !empty($data['coverImage']['extraLarge'])) {
|
||
$path = $this->downloadAndResize(
|
||
$data['coverImage']['extraLarge'],
|
||
"anime/covers/{$anime->id}.webp",
|
||
self::COVER_MAX_W
|
||
);
|
||
if ($path) $updates['cover_image'] = $path;
|
||
}
|
||
|
||
if ($needBanner && !empty($data['bannerImage'])) {
|
||
$path = $this->downloadAndResize(
|
||
$data['bannerImage'],
|
||
"anime/banners/{$anime->id}.webp",
|
||
self::BANNER_MAX_W
|
||
);
|
||
if ($path) $updates['banner_image'] = $path;
|
||
}
|
||
|
||
if (empty($updates)) return false;
|
||
|
||
$anime->update($updates);
|
||
return true;
|
||
}
|
||
}
|