Files
animexe/app/Http/Controllers/Admin/AiController.php
T
2026-07-14 00:01:48 +03:00

268 lines
9.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Anime;
use App\Models\Episode;
use App\Models\Genre;
use App\Models\Setting;
use App\Services\DeepSeekService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class AiController extends Controller
{
/**
* GET /admin/ai/anime-meta — toplu anime meta doldurma sayfası.
*/
public function animeMetaPage()
{
$total = Anime::count();
$missing = Anime::where(function ($q) {
$q->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]);
}
}