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