268 lines
9.6 KiB
PHP
268 lines
9.6 KiB
PHP
<?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]);
|
||
}
|
||
}
|