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
+174
View File
@@ -0,0 +1,174 @@
<?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;
}
}