whereNotNull('mal_id') ->where('mal_id', '>', 0) ->select('mal_id', DB::raw('COUNT(*) as cnt')) ->groupBy('mal_id') ->having('cnt', '>', 1) ->get() ->map(function ($row) { $animes = Anime::where('mal_id', $row->mal_id) ->withCount('episodes') ->get(['id', 'title', 'slug', 'mal_id', 'created_at']); return ['mal_id' => $row->mal_id, 'animes' => $animes]; }); // 2. Karışık kaynak — aynı anime içinde hem animecix hem anizium bölüm var $mixedSources = DB::table('episodes') ->whereIn('source', ['anizium', 'animecix']) ->whereNotNull('anime_id') ->select('anime_id', 'source', DB::raw('COUNT(*) as cnt')) ->groupBy('anime_id', 'source') ->get() ->groupBy('anime_id') ->filter(fn($group) => $group->pluck('source')->unique()->count() > 1) ->map(function ($group) { $anime = Anime::find($group->first()->anime_id, ['id', 'title', 'slug']); if (!$anime) return null; $sources = $group->mapWithKeys(fn($r) => [$r->source => $r->cnt]); return ['anime' => $anime, 'sources' => $sources]; }) ->filter() ->values(); // 3. Eksik bölümler — episode_count > gerçek bölüm sayısı $missingEpisodes = Anime::whereNotNull('episode_count') ->where('episode_count', '>', 0) ->withCount('episodes') ->get(['id', 'title', 'slug', 'episode_count']) ->filter(fn($a) => $a->episodes_count < $a->episode_count) ->map(fn($a) => [ 'id' => $a->id, 'title' => $a->title, 'slug' => $a->slug, 'expected' => $a->episode_count, 'actual' => $a->episodes_count, 'missing' => $a->episode_count - $a->episodes_count, ]) ->sortByDesc('missing') ->values(); // 4. Harici CDN bölümler — BunnyCDN'e taşınmamış, Anizium CDN'de kalan $externalCount = Episode::whereNull('video_url') ->where(function ($q) { $q->where('m3u8_url', 'like', '%aniziumserver%') ->orWhere('m3u8_url', 'like', '%anizium%'); }) ->count(); $externalSample = Episode::whereNull('video_url') ->where(function ($q) { $q->where('m3u8_url', 'like', '%aniziumserver%') ->orWhere('m3u8_url', 'like', '%anizium%'); }) ->with('anime:id,title,slug') ->select('id', 'anime_id', 'season_id', 'episode_number', 'm3u8_url', 'source') ->orderByDesc('id') ->limit(100) ->get(); // 5. Sıfır bölümlü animeler $zeroEpisodeAnimes = Anime::whereDoesntHave('episodes') ->get(['id', 'title', 'slug', 'created_at']); return view('admin.health.index', compact( 'malDuplicates', 'mixedSources', 'missingEpisodes', 'externalCount', 'externalSample', 'zeroEpisodeAnimes' )); } // ── Sistem Temizliği ───────────────────────────────────────────────────── /** * Depolama istatistiklerini döndür — inode tüketimini gösterir. */ public function storageStats() { $dirs = [ 'seg_cache' => storage_path('app/seg_cache'), 'cache_data' => storage_path('framework/cache/data'), 'sessions' => storage_path('framework/sessions'), 'views' => storage_path('framework/views'), 'logs' => storage_path('logs'), 'app_public' => storage_path('app/public'), ]; $stats = []; foreach ($dirs as $key => $path) { if (!is_dir($path)) { $stats[$key] = ['count' => 0, 'size' => 0, 'path' => $path]; continue; } $files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS)); $count = 0; $size = 0; foreach ($files as $f) { $count++; $size += $f->getSize(); } $stats[$key] = ['count' => $count, 'size' => $size, 'path' => $path]; } return response()->json(['stats' => $stats, 'total_files' => array_sum(array_column($stats, 'count'))]); } /** * Belirtilen depolama dizinini temizle. */ public function cleanupStorage(Request $request) { $target = $request->input('target'); $allowed = [ 'seg_cache' => storage_path('app/seg_cache'), 'cache_data' => storage_path('framework/cache/data'), 'sessions' => storage_path('framework/sessions'), 'views' => storage_path('framework/views'), 'old_logs' => storage_path('logs'), ]; if (!array_key_exists($target, $allowed)) { return response()->json(['error' => 'Geçersiz hedef.'], 422); } $path = $allowed[$target]; $deleted = 0; if (!is_dir($path)) { return response()->json(['ok' => true, 'deleted' => 0, 'message' => 'Dizin yok.']); } if ($target === 'old_logs') { // Logları tamamen silme — sadece 7 günden eskilerini sil foreach (glob($path . '/*.log') ?: [] as $f) { if (filemtime($f) < time() - 604800) { // 7 gün @unlink($f); $deleted++; } } // Laravel her gün yeni log açar, bugünküne dokunma } else { // Diğer dizinler: tümünü temizle $files = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST ); foreach ($files as $f) { if ($f->isFile()) { @unlink($f->getRealPath()); $deleted++; } elseif ($f->isDir()) { @rmdir($f->getRealPath()); } } } // Laravel cache'i PHP seviyesinde de temizle if ($target === 'cache_data') { try { \Illuminate\Support\Facades\Cache::flush(); } catch (\Throwable) {} } return response()->json([ 'ok' => true, 'deleted' => $deleted, 'message' => "{$deleted} dosya silindi.", ]); } /** * Session driver bilgisi + önerisi. */ public function sessionInfo() { $driver = config('session.driver', 'file'); $sessionPath = storage_path('framework/sessions'); $sessionCount = is_dir($sessionPath) ? count(glob($sessionPath . '/*') ?: []) : 0; return response()->json([ 'driver' => $driver, 'session_files' => $sessionCount, 'recommendation'=> $driver === 'file' ? 'SESSION_DRIVER=database veya cookie kullanmanız önerilir (inode tasarrufu).' : 'Session sürücüsü inode-dostu.', ]); } public function deleteAnime(Request $request, Anime $anime) { $title = $anime->title; $anime->delete(); return back()->with('success', "\"$title\" silindi."); } public function deleteSourceEpisodes(Request $request, Anime $anime) { $source = $request->validate(['source' => 'required|in:anizium,animecix'])['source']; $count = Episode::where('anime_id', $anime->id)->where('source', $source)->count(); Episode::where('anime_id', $anime->id)->where('source', $source)->delete(); return back()->with('success', "$anime->title — $source kaynağından $count bölüm silindi."); } }