413 lines
16 KiB
PHP
413 lines
16 KiB
PHP
<?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,
|
||
]);
|
||
}
|
||
}
|