whereNull('description')->orWhere('description', '') ->orWhereNull('release_year') ->orWhereNull('studio')->orWhere('studio', '') ->orWhereNull('type')->orWhere('type', '') ->orWhereNull('status')->orWhere('status', ''); })->count(); $noGenres = Anime::doesntHave('genres')->count(); return view('admin.ai.anime-meta', compact('total', 'missing', 'noGenres')); } /** * POST /admin/ai/anime-meta-ids — eksik animelerin ID listesini döndür. */ public function animeMetaIds(Request $request) { $force = $request->boolean('force'); $query = Anime::with('genres:id')->select('id', 'title', 'description', 'release_year', 'studio', 'type', 'status', 'rating', 'title_en', 'title_jp'); 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'); } $animes = $query->orderBy('id')->get()->map(function ($a) { $missing = []; if (empty($a->description)) $missing[] = 'açıklama'; if (empty($a->release_year)) $missing[] = 'yıl'; if (empty($a->studio)) $missing[] = 'stüdyo'; if (empty($a->type)) $missing[] = 'tür'; if (empty($a->status)) $missing[] = 'durum'; if (!$a->rating) $missing[] = 'puan'; if ($a->genres->isEmpty()) $missing[] = 'kategoriler'; return ['id' => $a->id, 'title' => $a->title, 'missing' => $missing]; }); return response()->json(['animes' => $animes]); } /** * POST /admin/ai/fill-anime-meta — tek anime için meta doldur ve kaydet. */ public function fillAnimeMeta(Request $request) { $anime = Anime::with('genres:id,name')->findOrFail($request->anime_id); $force = $request->boolean('force'); $ai = new DeepSeekService(); if (!$ai->isConfigured()) { return response()->json(['error' => 'DeepSeek API Key tanımlı değil.'], 422); } $meta = $ai->generateAnimeMeta($anime->title, $anime->title_jp ?? ''); if (!$meta) { Log::channel('daily')->warning("[FillAnimeMeta-UI] HATA [{$anime->id}] {$anime->title}"); return response()->json(['error' => 'DeepSeek boş yanıt döndürdü.'], 500); } $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); if (!empty($meta['rating']) && ($force || !$anime->rating)) { $updates['rating'] = min(10, max(0, (float) $meta['rating'])); } if (!empty($updates)) $anime->update($updates); $syncedGenres = []; if (!empty($meta['genres']) && ($force || $anime->genres->isEmpty())) { $ids = []; foreach ($meta['genres'] as $name) { $g = Genre::firstOrCreate(['name' => $name], ['slug' => Str::slug($name)]); $ids[] = $g->id; } if ($ids) { $force ? $anime->genres()->sync($ids) : $anime->genres()->syncWithoutDetaching($ids); $syncedGenres = $meta['genres']; } } $filled = array_keys($updates); if ($syncedGenres) $filled[] = 'kategoriler'; Log::channel('daily')->info("[FillAnimeMeta-UI] OK [{$anime->id}] {$anime->title} → " . implode(', ', $filled)); return response()->json([ 'ok' => true, 'filled' => $filled, 'meta' => array_merge($updates, ['genres' => $syncedGenres]), ]); } /** * Toplu açıklama yazma sayfası. */ public function descriptionsPage() { $animes = Anime::orderBy('title') ->withCount(['episodes as total_eps' => fn($q) => $q->whereNull('description')->orWhere('description', '')]) ->get() ->filter(fn($a) => $a->total_eps > 0); $totalMissing = Episode::where(fn($q) => $q->whereNull('description')->orWhere('description', ''))->count(); return view('admin.ai.descriptions', compact('animes', 'totalMissing')); } /** * Açıklaması olmayan bölüm ID'lerini döndür (JS için). * POST { anime_id: 0=tümü } */ public function episodeIds(Request $request) { $query = Episode::where(fn($q) => $q->whereNull('description')->orWhere('description', '')); if ($request->anime_id && $request->anime_id != '0') { $query->where('anime_id', $request->anime_id); } $ids = $query->with('anime:id,title')->get()->map(fn($ep) => [ 'id' => $ep->id, 'label' => ($ep->anime->title ?? '?') . ' — ' . $ep->episode_number . '. Bölüm' . ($ep->title ? ' — '.$ep->title : ''), ]); return response()->json(['episodes' => $ids]); } /** * Tek bir bölüme açıklama yaz ve kaydet. * POST { episode_id } */ public function fillOne(Request $request) { $episode = Episode::with('anime:id,title')->findOrFail($request->episode_id); if (!empty($episode->description)) { return response()->json(['ok' => true, 'skipped' => true, 'description' => $episode->description]); } $ai = new DeepSeekService(); if (!$ai->isConfigured()) { return response()->json(['error' => 'DeepSeek API Key tanımlı değil.'], 422); } $desc = $ai->generateEpisodeDescription( $episode->anime->title ?? 'Bilinmeyen', $episode->episode_number, $episode->title ?? '' ); if (!$desc) { return response()->json(['error' => 'DeepSeek boş yanıt döndürdü veya hata oluştu.'], 500); } $episode->update(['description' => $desc]); return response()->json(['ok' => true, 'description' => $desc]); } /** * Anime için tüm meta verileri AI ile doldur. * POST { anime_id, title?, title_jp? } * Döner: { description, release_year, studio, type, status, rating, title_en, title_jp, genres[] } */ public function animeMeta(Request $request) { $ai = new DeepSeekService(); if (!$ai->isConfigured()) { return response()->json(['error' => 'DeepSeek API Key tanımlı değil.'], 422); } $title = trim($request->input('title', '')); $titleJp = trim($request->input('title_jp', '')); if (!$title) { return response()->json(['error' => 'Başlık boş olamaz.'], 422); } $meta = $ai->generateAnimeMeta($title, $titleJp); if (!$meta) { return response()->json(['error' => 'DeepSeek boş yanıt döndürdü veya hata oluştu.'], 500); } return response()->json(['ok' => true, 'meta' => $meta]); } /** * DeepSeek ile Türkçe açıklama üret. * POST body: { type: 'anime'|'episode', title, title_jp?, anime_title?, episode_number?, genres? } */ public function generate(Request $request) { $ai = new DeepSeekService(); if (!$ai->isConfigured()) { return response()->json(['error' => 'DeepSeek API Key tanımlı değil. Ayarlar > DeepSeek bölümüne ekleyin.'], 422); } $type = $request->input('type', 'anime'); $title = trim($request->input('title', '')); if (!$title) { return response()->json(['error' => 'Başlık boş olamaz.'], 422); } if ($type === 'episode') { $desc = $ai->generateEpisodeDescription( trim($request->input('anime_title', $title)), (int) $request->input('episode_number', 1), trim($request->input('episode_title', '')) ); } else { $desc = $ai->generateAnimeDescription( $title, trim($request->input('title_jp', '')), trim($request->input('genres', '')) ); } if (!$desc) { return response()->json(['error' => 'DeepSeek boş yanıt döndürdü veya bağlantı hatası.'], 500); } return response()->json(['description' => $desc]); } }