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

413 lines
16 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\Season;
use App\Models\ContentPermission;
use App\Models\Genre;
use App\Models\PermissionSetting;
use App\Services\JikanService;
use App\Support\ImageOptimizer;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class AnimeController extends Controller
{
public function index(Request $request)
{
$query = Anime::with('genres')->latest();
if ($request->search) {
$query->where('title', 'like', '%' . $request->search . '%');
}
if ($request->status) {
$query->where('status', $request->status);
}
if ($request->type) {
$query->where('type', $request->type);
}
if ($request->no_episodes) {
$query->whereDoesntHave('episodes');
}
$animes = $query->paginate(20)->withQueryString();
$zeroEpisodeCount = Anime::whereDoesntHave('episodes')->count();
return view('admin.animes.index', compact('animes', 'zeroEpisodeCount'));
}
public function destroyZeroEpisodes()
{
$animes = Anime::whereDoesntHave('episodes')->get();
$count = $animes->count();
foreach ($animes as $anime) {
$anime->delete();
}
return response()->json(['success' => true, 'count' => $count]);
}
public function create()
{
$genres = Genre::where('is_active', true)->get();
$permissions = PermissionSetting::all();
return view('admin.animes.create', compact('genres', 'permissions'));
}
public function store(Request $request)
{
$data = $request->validate([
'title' => 'required|string|max:255',
'title_en' => 'nullable|string|max:255',
'title_jp' => 'nullable|string|max:255',
'description' => 'nullable|string',
'type' => 'required|in:series,movie,ova,ona,special',
'status' => 'required|in:ongoing,completed,upcoming',
'release_year' => 'nullable|integer|min:1900|max:2099',
'studio' => 'nullable|string|max:255',
'rating' => 'nullable|numeric|min:0|max:10',
'mal_id' => 'nullable|string|max:50',
'trailer_url' => 'nullable|url',
'is_featured' => 'boolean',
'is_published' => 'boolean',
'is_dubbed' => 'boolean',
]);
$data['slug'] = Str::slug($data['title']);
$data['is_featured'] = $request->boolean('is_featured');
$data['is_published'] = $request->boolean('is_published');
$data['is_dubbed'] = $request->boolean('is_dubbed');
// Auto-fetch MAL ID if not provided
if (empty($data['mal_id'])) {
try {
$data['mal_id'] = (new JikanService())->searchMalId(
$data['title'],
$data['title_en'] ?? null,
$data['title_jp'] ?? null,
$data['type'] ?? null,
);
} catch (\Throwable) {}
}
if ($request->hasFile('cover_image')) {
$data['cover_image'] = ImageOptimizer::store($request->file('cover_image'), 'covers', 'cover');
}
if ($request->hasFile('banner_image')) {
$data['banner_image'] = ImageOptimizer::store($request->file('banner_image'), 'banners', 'banner');
}
$anime = Anime::create($data);
if ($request->genres) {
$anime->genres()->sync($request->genres);
}
// Auto-fill season MAL IDs if mal_id was found
if ($anime->mal_id) {
dispatch(function () use ($anime) {
try {
$chain = (new JikanService())->fetchSeasonMalIds($anime->mal_id);
foreach ($anime->seasons()->orderBy('season_number')->get() as $i => $season) {
if (isset($chain[$i])) $season->update(['mal_id' => $chain[$i]]);
}
} catch (\Throwable) {}
})->afterResponse();
}
return redirect()->route('admin.animes.show', $anime)->with('success', 'Anime eklendi.');
}
public function show(Anime $anime)
{
$anime->load(['genres', 'seasons.episodes']);
$permissions = PermissionSetting::all();
$contentPerms = ContentPermission::where('content_type', 'anime')
->where('content_id', $anime->id)
->pluck('required_membership', 'permission_key');
return view('admin.animes.show', compact('anime', 'permissions', 'contentPerms'));
}
public function edit(Anime $anime)
{
$genres = Genre::where('is_active', true)->get();
$permissions = PermissionSetting::all();
$contentPerms = ContentPermission::where('content_type', 'anime')
->where('content_id', $anime->id)
->pluck('required_membership', 'permission_key');
return view('admin.animes.edit', compact('anime', 'genres', 'permissions', 'contentPerms'));
}
public function update(Request $request, Anime $anime)
{
$data = $request->validate([
'title' => 'required|string|max:255',
'title_en' => 'nullable|string|max:255',
'title_jp' => 'nullable|string|max:255',
'description' => 'nullable|string',
'type' => 'required|in:series,movie,ova,ona,special',
'status' => 'required|in:ongoing,completed,upcoming',
'release_year' => 'nullable|integer|min:1900|max:2099',
'studio' => 'nullable|string|max:255',
'rating' => 'nullable|numeric|min:0|max:10',
'mal_id' => 'nullable|string|max:50',
'trailer_url' => 'nullable|url',
'is_featured' => 'boolean',
'is_published' => 'boolean',
'is_dubbed' => 'boolean',
]);
$data['is_featured'] = $request->boolean('is_featured');
$data['is_published'] = $request->boolean('is_published');
$data['is_dubbed'] = $request->boolean('is_dubbed');
// Auto-fetch MAL ID if not provided and anime doesn't already have one
if (empty($data['mal_id']) && empty($anime->mal_id)) {
try {
$data['mal_id'] = (new JikanService())->searchMalId(
$data['title'],
$data['title_en'] ?? null,
$data['title_jp'] ?? null,
);
} catch (\Throwable) {}
}
if ($request->hasFile('cover_image')) {
ImageOptimizer::delete($anime->cover_image);
$data['cover_image'] = ImageOptimizer::store($request->file('cover_image'), 'covers', 'cover');
}
if ($request->hasFile('banner_image')) {
ImageOptimizer::delete($anime->banner_image);
$data['banner_image'] = ImageOptimizer::store($request->file('banner_image'), 'banners', 'banner');
}
$anime->update($data);
if ($request->has('genres')) {
$anime->genres()->sync($request->genres ?? []);
}
// MAL ID değiştiyse: AniSkip cache'lerini temizle + sezon MAL ID'lerini doldur
if ($anime->mal_id) {
dispatch(function () use ($anime) {
try {
// AniSkip null cache'lerini temizle (tüm bölümler için)
foreach ($anime->seasons as $s) {
if ($s->mal_id) {
foreach ($anime->episodes()->where('season_id', $s->id)->pluck('episode_number') as $epNum) {
\Illuminate\Support\Facades\Cache::forget("aniskip_{$s->mal_id}_{$epNum}");
}
}
}
// S1 için doğrudan anime.mal_id kullan
$s1 = $anime->seasons()->where('season_number', 1)->first();
if ($s1 && !$s1->mal_id) {
$s1->update(['mal_id' => $anime->mal_id]);
foreach ($anime->episodes()->where('season_id', $s1->id)->pluck('episode_number') as $epNum) {
\Illuminate\Support\Facades\Cache::forget("aniskip_{$anime->mal_id}_{$epNum}");
}
}
// S2+ için Jikan chain
$chain = (new JikanService())->fetchSeasonMalIds($anime->mal_id);
\Illuminate\Support\Facades\Cache::put("jikan_chain_{$anime->mal_id}", $chain, 60 * 60 * 24 * 7);
foreach ($anime->seasons()->orderBy('season_number')->get() as $i => $season) {
if (!$season->mal_id && isset($chain[$i])) {
$season->update(['mal_id' => $chain[$i]]);
}
}
} catch (\Throwable) {}
})->afterResponse();
}
return redirect()->route('admin.animes.show', $anime)->with('success', 'Anime güncellendi.');
}
public function destroy(Anime $anime)
{
// CDN klasörü için örnek bir video_url al (anime_XXXXX/ path'ini çıkarmak için)
$sampleVideoUrl = $anime->episodes()->whereNotNull('video_url')->value('video_url');
$anime->delete();
// CDN'den tüm anime klasörünü arka planda sil (anime_XXXXX/season_X/...)
dispatch(function () use ($sampleVideoUrl) {
\App\Services\BunnyCdnStorage::deleteAnimeFolder($sampleVideoUrl);
})->afterResponse();
return redirect()->route('admin.animes.index')->with('success', 'Anime silindi.');
}
public function updatePermissions(Request $request, Anime $anime)
{
$permissions = $request->permissions ?? [];
// Mevcut override'ları sil
ContentPermission::where('content_type', 'anime')
->where('content_id', $anime->id)
->delete();
// Yeni override'ları kaydet
foreach ($permissions as $key => $value) {
if (in_array($value, ['free', 'premium'])) {
ContentPermission::create([
'content_type' => 'anime',
'content_id' => $anime->id,
'permission_key' => $key,
'required_membership' => $value,
]);
}
}
return back()->with('success', 'İzinler güncellendi.');
}
/**
* POST admin/animes/{anime}/fetch-mal-seasons
* Walks the Jikan sequel chain and fills seasons.mal_id automatically.
*/
public function fetchMalSeasons(Request $request, Anime $anime)
{
// Formdan gelen mal_id varsa önce güncelle
if ($request->filled('mal_id')) {
$anime->update(['mal_id' => $request->input('mal_id')]);
}
if (!$anime->mal_id) {
return response()->json(['error' => 'MAL ID girilmemiş. MyAnimeList.net\'ten anime sayfasını açıp URL\'deki numarayı gir.'], 422);
}
$jikan = new JikanService();
$chain = $jikan->fetchSeasonMalIds($anime->mal_id);
if (empty($chain)) {
return response()->json(['error' => 'Jikan API\'den veri alınamadı.'], 502);
}
$seasons = Season::where('anime_id', $anime->id)
->orderBy('season_number')
->get();
$updated = [];
foreach ($seasons as $index => $season) {
$malId = $chain[$index] ?? null;
if ($malId) {
$season->update(['mal_id' => $malId]);
$updated[] = [
'season' => $season->season_number,
'mal_id' => $malId,
];
}
}
// If anime has more seasons than chain entries, remaining seasons stay null
return response()->json([
'success' => true,
'chain' => $chain,
'updated' => $updated,
'message' => count($updated) . ' sezon güncellendi.',
]);
}
/**
* POST admin/animes/{anime}/fetch-mal
* Tek bir anime için MAL ID arar ve kaydeder.
*/
public function fetchMalSingle(Anime $anime)
{
try {
$malId = (new JikanService())->searchMalId(
$anime->title, $anime->title_en, $anime->title_jp, $anime->type
);
if ($malId) {
$anime->update(['mal_id' => $malId]);
$s1 = $anime->seasons()->where('season_number', 1)->first();
if ($s1 && !$s1->mal_id) $s1->update(['mal_id' => $malId]);
return response()->json(['found' => true, 'mal_id' => $malId]);
}
return response()->json(['found' => false]);
} catch (\Throwable $e) {
return response()->json(['found' => false, 'error' => $e->getMessage()], 500);
}
}
public function bulkDestroy(Request $request)
{
if ($request->boolean('all')) {
$query = Anime::query();
$f = $request->input('filters', []);
if (!empty($f['search'])) $query->where('title', 'like', '%'.$f['search'].'%');
if (!empty($f['status'])) $query->where('status', $f['status']);
if (!empty($f['type'])) $query->where('type', $f['type']);
$animes = $query->get();
} else {
$request->validate(['ids' => 'required|array', 'ids.*' => 'integer']);
$animes = Anime::whereIn('id', $request->ids)->get();
}
$sampleUrls = [];
foreach ($animes as $anime) {
$url = $anime->episodes()->whereNotNull('video_url')->value('video_url');
if ($url) $sampleUrls[] = $url;
$anime->delete();
}
dispatch(function () use ($sampleUrls) {
foreach ($sampleUrls as $url) {
\App\Services\BunnyCdnStorage::deleteAnimeFolder($url);
}
})->afterResponse();
return response()->json(['success' => true, 'deleted' => count($animes)]);
}
/**
* POST admin/animes/bulk-find-mal
* MAL ID'si olmayan animeleri Jikan title search ile toplu doldurur.
* Her seferinde 1 anime işler (AJAX loop), Jikan rate limit aşılmaz.
*/
public function bulkFindMal(Request $request)
{
$skipIds = $request->input('skip_ids', []);
$anime = Anime::where(fn($q) => $q->whereNull('mal_id')->orWhere('mal_id', ''))
->when($skipIds, fn($q) => $q->whereNotIn('id', $skipIds))
->orderBy('id')
->first();
if (!$anime) {
return response()->json(['done' => true, 'message' => 'Tüm animelerin MAL ID\'si dolu!']);
}
$jikan = new JikanService();
$malId = $jikan->searchMalId($anime->title, $anime->title_en, $anime->title_jp, $anime->type);
if ($malId) {
$anime->update(['mal_id' => $malId]);
// S1 için season.mal_id de doldur
$s1 = $anime->seasons()->where('season_number', 1)->first();
if ($s1 && !$s1->mal_id) $s1->update(['mal_id' => $malId]);
return response()->json([
'done' => false,
'found' => true,
'anime' => $anime->title,
'mal_id' => $malId,
'remaining' => Anime::whereNull('mal_id')->orWhere('mal_id', '')->count(),
]);
}
// Bulunamadı — bir sonrakine geç (geçici olarak dummy değer koy, sonra temizle)
return response()->json([
'done' => false,
'found' => false,
'anime' => $anime->title,
'mal_id' => null,
'remaining' => Anime::whereNull('mal_id')->orWhere('mal_id', '')->count() - 1,
'skipped_id' => $anime->id,
]);
}
}