Initial commit: Animexe Laravel platform

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 00:01:48 +03:00
co-authored by Claude Opus 4.8
commit a63515cfc6
366 changed files with 74773 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
<?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;
}
}