175 lines
6.9 KiB
PHP
175 lines
6.9 KiB
PHP
<?php
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use App\Models\Anime;
|
||
use App\Models\Genre;
|
||
use App\Services\DeepSeekService;
|
||
use Illuminate\Console\Command;
|
||
use Illuminate\Support\Facades\Log;
|
||
|
||
class FillAnimeMeta extends Command
|
||
{
|
||
protected $signature = 'animexe:fill-anime-meta
|
||
{--limit=0 : Max anime sayısı (0 = sınırsız, hepsi)}
|
||
{--anime= : Sadece belirli bir anime ID işle}
|
||
{--force : Dolu alanlar olsa bile yeniden doldur}
|
||
{--dry-run : Gerçekten kaydetme, sadece ne yapılacağını göster}';
|
||
|
||
protected $description = 'Eksik meta verisi olan animeleri DeepSeek AI ile toplu doldurur (description, genres, studio, year, vb.)';
|
||
|
||
public function handle(DeepSeekService $ai): int
|
||
{
|
||
// PHP zaman aşımını kaldır — tüm animeler işlenene kadar çalışsın
|
||
set_time_limit(0);
|
||
|
||
if (!$ai->isConfigured()) {
|
||
$this->error('DeepSeek API anahtarı ayarlanmamış. Admin > Ayarlar > deepseek_api_key');
|
||
return self::FAILURE;
|
||
}
|
||
|
||
$limit = (int) $this->option('limit');
|
||
$animeId = $this->option('anime');
|
||
$force = $this->option('force');
|
||
$dry = $this->option('dry-run');
|
||
|
||
// İşlenecek animeleri belirle
|
||
if ($animeId) {
|
||
$animes = Anime::where('id', $animeId)->with('genres')->get();
|
||
} else {
|
||
$query = Anime::with('genres');
|
||
|
||
if (!$force) {
|
||
$query->where(function ($q) {
|
||
$q->whereNull('description')->orWhere('description', '')
|
||
->orWhereNull('release_year')
|
||
->orWhereNull('studio')->orWhere('studio', '')
|
||
->orWhereNull('type')->orWhere('type', '')
|
||
->orWhereNull('status')->orWhere('status', '');
|
||
})->orDoesntHave('genres');
|
||
}
|
||
|
||
$query->orderBy('id');
|
||
if ($limit > 0) $query->limit($limit);
|
||
$animes = $query->get();
|
||
}
|
||
|
||
if ($animes->isEmpty()) {
|
||
$this->info('İşlenecek anime bulunamadı (tüm alanlar dolu).');
|
||
Log::channel('daily')->info('[FillAnimeMeta] İşlenecek anime yok.');
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
$this->info("Toplam {$animes->count()} anime işlenecek" . ($dry ? ' (dry-run)' : '') . '...');
|
||
Log::channel('daily')->info("[FillAnimeMeta] Başladı. {$animes->count()} anime, limit={$limit}, force=" . ($force ? 'evet' : 'hayır'));
|
||
|
||
$done = 0;
|
||
$skipped = 0;
|
||
$failed = 0;
|
||
|
||
foreach ($animes as $anime) {
|
||
$missing = $this->missingFields($anime);
|
||
|
||
if (!$force && empty($missing)) {
|
||
$this->line(" <fg=gray>ATLA</> {$anime->title} — tüm alanlar dolu");
|
||
$skipped++;
|
||
continue;
|
||
}
|
||
|
||
$label = $force ? 'tüm alanlar' : implode(', ', $missing);
|
||
$this->line(" <fg=cyan>İŞLE</> [{$anime->id}] {$anime->title} — eksik: {$label}");
|
||
|
||
if ($dry) {
|
||
$done++;
|
||
continue;
|
||
}
|
||
|
||
$meta = $ai->generateAnimeMeta($anime->title, $anime->title_jp ?? '');
|
||
|
||
if (!$meta) {
|
||
$this->warn(" <fg=red>HATA</> {$anime->title} — AI boş yanıt döndürdü");
|
||
Log::channel('daily')->warning("[FillAnimeMeta] HATA [{$anime->id}] {$anime->title}: AI boş yanıt");
|
||
$failed++;
|
||
sleep(3);
|
||
continue;
|
||
}
|
||
|
||
// Sadece boş alanları doldur (force modunda hepsini güncelle)
|
||
$updates = [];
|
||
|
||
$fillIfEmpty = function (string $field, $value) use ($anime, $force, &$updates) {
|
||
if ($value === null || $value === '') return;
|
||
if ($force || empty($anime->$field)) {
|
||
$updates[$field] = $value;
|
||
}
|
||
};
|
||
|
||
$fillIfEmpty('description', $meta['description'] ?? null);
|
||
$fillIfEmpty('release_year', $meta['release_year'] ?? null);
|
||
$fillIfEmpty('studio', $meta['studio'] ?? null);
|
||
$fillIfEmpty('type', $meta['type'] ?? null);
|
||
$fillIfEmpty('status', $meta['status'] ?? null);
|
||
$fillIfEmpty('title_en', $meta['title_en'] ?? null);
|
||
$fillIfEmpty('title_jp', $meta['title_jp'] ?? null);
|
||
|
||
// Rating: sadece boşsa veya 0 ise doldur
|
||
if (!empty($meta['rating']) && ($force || !$anime->rating)) {
|
||
$updates['rating'] = min(10, max(0, (float) $meta['rating']));
|
||
}
|
||
|
||
if (!empty($updates)) {
|
||
$anime->update($updates);
|
||
}
|
||
|
||
// Genres: boşsa ekle
|
||
if (!empty($meta['genres']) && ($force || $anime->genres->isEmpty())) {
|
||
$genreIds = [];
|
||
foreach ($meta['genres'] as $genreName) {
|
||
$genre = Genre::firstOrCreate(
|
||
['name' => $genreName],
|
||
['slug' => \Illuminate\Support\Str::slug($genreName)]
|
||
);
|
||
$genreIds[] = $genre->id;
|
||
}
|
||
if ($genreIds) {
|
||
$force ? $anime->genres()->sync($genreIds) : $anime->genres()->syncWithoutDetaching($genreIds);
|
||
}
|
||
}
|
||
|
||
$updatedFields = array_keys($updates);
|
||
if (!empty($meta['genres']) && ($force || $anime->genres->isEmpty())) {
|
||
$updatedFields[] = 'genres(' . implode(',', $meta['genres'] ?? []) . ')';
|
||
}
|
||
|
||
$summary = empty($updatedFields) ? 'Yeni alan yok' : implode(', ', $updatedFields);
|
||
$this->info(" <fg=green>✓ OK</> [{$anime->id}] {$anime->title} → {$summary}");
|
||
Log::channel('daily')->info("[FillAnimeMeta] OK [{$anime->id}] {$anime->title} → {$summary}");
|
||
|
||
$done++;
|
||
|
||
// API rate limit — DeepSeek'i boğma
|
||
sleep(2);
|
||
}
|
||
|
||
$summary = "Tamamlandı: {$done} işlendi, {$skipped} atlandı, {$failed} hata.";
|
||
$this->info($summary);
|
||
Log::channel('daily')->info("[FillAnimeMeta] {$summary}");
|
||
|
||
return $failed > 0 ? self::FAILURE : self::SUCCESS;
|
||
}
|
||
|
||
private function missingFields(Anime $anime): array
|
||
{
|
||
$missing = [];
|
||
if (empty($anime->description)) $missing[] = 'description';
|
||
if (empty($anime->release_year)) $missing[] = 'release_year';
|
||
if (empty($anime->studio)) $missing[] = 'studio';
|
||
if (empty($anime->type)) $missing[] = 'type';
|
||
if (empty($anime->status)) $missing[] = 'status';
|
||
if (!$anime->rating) $missing[] = 'rating';
|
||
if (empty($anime->title_en)) $missing[] = 'title_en';
|
||
if ($anime->genres->isEmpty()) $missing[] = 'genres';
|
||
return $missing;
|
||
}
|
||
}
|