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, ]); } }