timestamp / 900); // 15 dk = 900 sn // ── Latest + Top Rated (cache'li) ──────────────────────────────────── $latest = cache()->remember("home.latest.{$rotationSlot}", 900, fn() => Anime::where('is_published', true)->latest()->take(20)->get() ); $topRated = cache()->remember("home.toprated.{$rotationSlot}", 900, fn() => Anime::where('is_published', true)->where('rating', '>=', 7) ->orderByDesc('rating')->take(14)->get() ); $genres = cache()->remember('home.genres', 3600, fn() => Genre::where('is_active', true) ->withCount(['animes' => fn($q) => $q->where('is_published', true)]) ->orderByDesc('animes_count') ->take(16)->get() ); $newEpisodes = cache()->remember("home.newepisodes.{$rotationSlot}", 900, fn() => Episode::with(['anime', 'season']) ->where('is_published', true) ->latest()->take(14)->get() ); // ── Trending: YouTube-benzeri skor ─────────────────────────────────── $trending = cache()->remember("home.trending.{$rotationSlot}", 900, function () use ($latest) { try { // trending_score kolonu varsa kullan (migration çalıştırıldıysa) $byScore = Anime::where('is_published', true) ->where(fn($q) => $q->where('trending_score', '>', 0)->orWhere('is_trending', true)) ->orderByDesc('trending_score') ->take(12) ->get(); if ($byScore->count() >= 6) return $byScore; } catch (\Throwable) {} // Fallback: manuel + view_count bazlı $manual = Anime::where('is_trending', true)->where('is_published', true) ->orderBy('trending_order')->take(12)->get(); if ($manual->count() >= 6) return $manual->take(12); $autoFill = Anime::where('is_published', true) ->whereNotIn('id', $manual->pluck('id')) ->withSum(['episodes as recent_views' => fn($q) => $q->where('is_published', true)->where('updated_at', '>=', now()->subDays(30)) ], 'view_count') ->orderByDesc('recent_views') ->take(12 - $manual->count())->get(); $merged = $manual->concat($autoFill); return $merged->isEmpty() ? $latest->take(12) : $merged; }); // Rotasyon: top 8 sabit, son 4 her 15dk'da shuffle $top8 = $trending->take(8)->values(); $bottom4 = $trending->slice(8)->shuffle()->values(); $trending = $top8->concat($bottom4)->take(12)->values(); // ── Devam Ediyor (Bu Sezon) ─────────────────────────────────────────── $ongoing = cache()->remember("home.ongoing.{$rotationSlot}", 900, fn() => Anime::where('is_published', true)->where('status', 'ongoing') ->orderByDesc('rating')->take(12)->get() ); // ── Popüler Filmler ─────────────────────────────────────────────────── $popularMovies = cache()->remember("home.movies.{$rotationSlot}", 900, fn() => Anime::where('is_published', true)->where('type', 'movie') ->where('rating', '>=', 6)->orderByDesc('rating')->take(12)->get() ); // ── Türkçe Dublaj ───────────────────────────────────────────────────── $dubbed = cache()->remember("home.dubbed.{$rotationSlot}", 900, fn() => Anime::where('is_published', true)->where('is_dubbed', true) ->orderByDesc('rating')->take(20)->get() ); // ── Tür Spotlight (2 farklı tür, her birinde top 8 anime) ──────────── $genreSpotlights = cache()->remember("home.genre_spots.{$rotationSlot}", 900, function () { $spotGenres = Genre::where('is_active', true) ->whereIn('name', ['Aksiyon', 'Fantezi', 'Romantik', 'Psikolojik', 'Komedi', 'Spor', 'Macera', 'Drama']) ->inRandomOrder()->take(3)->get(); return $spotGenres->map(fn($g) => [ 'genre' => $g, 'animes' => $g->animes() ->where('is_published', true) ->where('rating', '>=', 6) ->orderByDesc('rating') ->take(8)->get(), ])->filter(fn($s) => $s['animes']->count() >= 3)->values(); }); // ── Featured Hero Slider: YouTube-benzeri trending algoritması ───────── $featured = cache()->remember("home.featured.{$rotationSlot}", 900, function () use ($trending, $topRated, $latest) { // trending_score kolonu var mı? (migration çalıştırılmamışsa fallback) $hasTrendingScore = \Illuminate\Support\Facades\Schema::hasColumn('animes', 'trending_score'); $orderBy = fn($q) => $hasTrendingScore ? $q->orderByDesc('trending_score') : $q->orderByDesc('rating'); $used = collect(); // TIER 1: Son 24 saatte yeni bölüm + trend skoru yüksek + banner try { $tier1Ids = Episode::where('is_published', true) ->where('created_at', '>=', now()->subDay()) ->pluck('anime_id')->unique()->toArray(); $tier1 = $orderBy(Anime::where('is_published', true) ->whereIn('id', $tier1Ids) ->whereNotNull('banner_image')) ->take(6)->get(); $used = $used->concat($tier1->pluck('id')); } catch (\Throwable) { $tier1 = collect(); } // TIER 2: Son 3 günde yeni bölüm + banner $tier2 = collect(); if ($tier1->count() < 6) { try { $tier2Ids = Episode::where('is_published', true) ->where('created_at', '>=', now()->subDays(3)) ->pluck('anime_id')->unique()->diff($used)->toArray(); $tier2 = $orderBy(Anime::where('is_published', true) ->whereIn('id', $tier2Ids) ->whereNotNull('banner_image')) ->take(6 - $tier1->count())->get(); $used = $used->concat($tier2->pluck('id')); } catch (\Throwable) {} } $combined = $tier1->concat($tier2); // TIER 3: Yüksek skor + banner if ($combined->count() < 6) { try { $tier3 = $orderBy(Anime::where('is_published', true) ->whereNotIn('id', $used->toArray()) ->whereNotNull('banner_image')) ->take(6 - $combined->count())->get(); $used = $used->concat($tier3->pluck('id')); $combined = $combined->concat($tier3); } catch (\Throwable) {} } // TIER 4: Trending + topRated (banner olmadan) if ($combined->count() < 5) { $fill = $trending->whereNotIn('id', $used->toArray())->take(5 - $combined->count()); $combined = $combined->concat($fill); } if ($combined->count() < 5) { $fill2 = $topRated->whereNotIn('id', $combined->pluck('id'))->take(5 - $combined->count()); $combined = $combined->concat($fill2); } return $combined->isEmpty() ? $latest->take(5)->values() : $combined->values(); }); $featured->load('genres', 'seasons', 'episodes'); // ── Hero slider JSON ────────────────────────────────────────────────── $statusLabel = ['ongoing' => 'Devam Ediyor', 'completed' => 'Tamamlandı', 'upcoming' => 'Yakında']; $featuredSlider = $featured->values()->map(function ($a) use ($statusLabel) { $firstSeason = $a->seasons->sortBy('season_number')->first(); $firstEp = $firstSeason ? $a->episodes->where('season_id', $firstSeason->id)->where('is_published', true)->sortBy('episode_number')->first() : null; return [ 'title' => $a->title, 'description' => $a->description, 'rating' => $a->rating, 'year' => $a->release_year, 'episodes' => $a->episode_count, 'status' => $a->status, 'statusLabel' => $statusLabel[$a->status] ?? $a->status, 'slug' => $a->slug, 'genres' => $a->genres->pluck('name')->values(), 'coverUrl' => $a->coverUrl, 'bannerUrl' => $a->bannerUrl, 'studio' => $a->studio, 'watchUrl' => ($firstSeason && $firstEp) ? route('watch', [$a->slug, $firstSeason->season_number, $firstEp->episode_number]) : null, 'detailUrl' => route('anime.show', $a->slug), ]; })->toArray(); // ── Devam Et (auth) ─────────────────────────────────────────────────── $continueWatching = collect(); $recommended = collect(); $userWatchTitles = ''; if (auth()->check()) { try { $continueWatching = ContinueWatching::where('user_id', auth()->id()) ->with('anime:id,title,slug,cover_image') ->where('percent_complete', '>=', 5) ->where('percent_complete', '<', 95) ->orderByDesc('updated_at') ->limit(12) ->get(); $watchedIds = ContinueWatching::where('user_id', auth()->id())->pluck('anime_id'); if ($watchedIds->isNotEmpty()) { $topGenreIds = DB::table('anime_genre') ->whereIn('anime_id', $watchedIds) ->select('genre_id', DB::raw('count(*) as cnt')) ->groupBy('genre_id') ->orderByDesc('cnt') ->limit(5) ->pluck('genre_id'); if ($topGenreIds->isNotEmpty()) { $recommended = Anime::where('is_published', true) ->whereNotIn('id', $watchedIds) ->whereHas('genres', fn($q) => $q->whereIn('genres.id', $topGenreIds)) ->inRandomOrder() ->take(14) ->get(); // Yeterli değilse rating'e göre topRated'dan dolduralım if ($recommended->count() < 6) { $fallback = Anime::where('is_published', true) ->whereNotIn('id', $watchedIds->merge($recommended->pluck('id'))) ->where('rating', '>=', 6) ->inRandomOrder() ->take(14 - $recommended->count()) ->get(); $recommended = $recommended->concat($fallback)->shuffle()->values(); } } $userWatchTitles = Anime::whereIn('id', $watchedIds->take(8)) ->pluck('title')->join(', '); } } catch (\Throwable $e) {} } // ── Stats ───────────────────────────────────────────────────────────── $statsAnime = cache()->remember('home.stats.anime', 3600, fn() => Anime::where('is_published', true)->count()); $statsEpisode = cache()->remember('home.stats.episode', 3600, fn() => Episode::where('is_published', true)->count()); $statsUser = cache()->remember('home.stats.user', 3600, fn() => User::count()); $statsGenre = cache()->remember('home.stats.genre', 3600, fn() => Genre::where('is_active', true)->count()); $apkUrl = \App\Models\Setting::get('mobile_apk_url', ''); // ── Banner reklamlar (premium görmez) ──────────────────────────────── $bannerAds = ['home_mid' => null, 'home_bottom' => null]; if (\App\Models\Setting::get('banner_ads_enabled', '0') === '1' && !(auth()->check() && auth()->user()->isPremium())) { try { $bannerAds['home_mid'] = \App\Models\Ad::pickBanner('home_mid'); $bannerAds['home_bottom'] = \App\Models\Ad::pickBanner('home_bottom'); } catch (\Throwable $e) {} } return view('frontend.home', compact( 'featured', 'featuredSlider', 'latest', 'topRated', 'genres', 'newEpisodes', 'trending', 'continueWatching', 'recommended', 'userWatchTitles', 'statsAnime', 'statsEpisode', 'statsUser', 'statsGenre', 'ongoing', 'popularMovies', 'genreSpotlights', 'dubbed', 'apkUrl', 'bannerAds' )); } public function search() { $q = request('q', ''); $genre = request('genre'); $type = request('type'); $status = request('status'); $year = request('year'); $sort = request('sort', 'popular'); $query = Anime::where('is_published', true) ->whereNotNull('slug') ->where('slug', '!=', ''); if ($q) { $query->where(function ($qb) use ($q) { $qb->where('title', 'like', "%$q%") ->orWhere('title_en', 'like', "%$q%") ->orWhere('title_jp', 'like', "%$q%"); }); } if ($genre) { $query->whereHas('genres', fn($qb) => $qb->where('slug', $genre)); } if ($type) { $query->where('type', $type); } if ($status) { $query->where('status', $status); } if ($year) { $query->where('release_year', $year); } // JSON autocomplete modu if (request()->boolean('json') || request()->expectsJson()) { $animes = $query->select('id', 'title', 'title_en', 'cover_image', 'type') ->latest()->limit(8)->get() ->map(fn($a) => [ 'id' => $a->id, 'title' => $a->title, 'cover' => $a->coverUrl, 'type' => $a->type, ]); return response()->json(['animes' => $animes]); } // ── Sıralama ────────────────────────────────────────────────────────── switch ($sort) { case 'popular': $query->withSum(['episodes as total_views' => fn($q) => $q->where('is_published', true) ], 'view_count')->orderByDesc('total_views'); break; case 'rating': $query->orderByDesc('rating')->orderByDesc('created_at'); break; case 'newest': $query->orderByDesc('release_year')->orderByDesc('created_at'); break; case 'oldest': $query->orderBy('release_year')->orderBy('created_at'); break; case 'az': $query->orderBy('title'); break; case 'za': $query->orderByDesc('title'); break; case 'personalized': if (auth()->check()) { $watchedIds = \App\Models\ContinueWatching::where('user_id', auth()->id()) ->pluck('anime_id'); $topGenreIds = $watchedIds->isNotEmpty() ? DB::table('anime_genre') ->whereIn('anime_id', $watchedIds) ->select('genre_id', DB::raw('count(*) as cnt')) ->groupBy('genre_id') ->orderByDesc('cnt') ->limit(6) ->pluck('genre_id') : collect(); if ($topGenreIds->isNotEmpty()) { $matchingIds = DB::table('anime_genre') ->whereIn('genre_id', $topGenreIds) ->pluck('anime_id') ->unique() ->values(); $idList = $matchingIds->isEmpty() ? '0' : $matchingIds->join(','); $query->orderByRaw("CASE WHEN animes.id IN ($idList) THEN 0 ELSE 1 END") ->orderByDesc('rating'); } else { $query->orderByDesc('rating'); } } else { $query->orderByDesc('rating'); } break; default: $query->orderByDesc('created_at'); } $results = $query->paginate(24)->withQueryString(); $genres = Genre::where('is_active', true)->get(); $years = Anime::where('is_published', true)->whereNotNull('release_year') ->distinct()->orderByDesc('release_year')->pluck('release_year'); return view('frontend.search', compact( 'results', 'genres', 'years', 'q', 'genre', 'type', 'status', 'year', 'sort' )); } public function searchSuggest() { $q = trim(request('q', '')); if (strlen($q) < 2) { return response()->json(['results' => []]); } $animes = Anime::where('is_published', true) ->where(function ($qb) use ($q) { $qb->where('title', 'like', "%$q%") ->orWhere('title_en', 'like', "%$q%") ->orWhere('title_jp', 'like', "%$q%"); }) ->select('id', 'title', 'title_en', 'slug', 'cover_image', 'type', 'release_year', 'episode_count') ->orderByRaw("CASE WHEN title LIKE ? THEN 0 ELSE 1 END, title ASC", ["$q%"]) ->limit(7) ->get() ->map(fn($a) => [ 'title' => $a->title, 'title_en' => $a->title_en, 'slug' => $a->slug, 'cover' => $a->coverUrl, 'type' => $a->type, 'year' => $a->release_year, 'episodes' => $a->episode_count, ]); return response()->json(['results' => $animes]); } public function genre(Genre $genre) { $animes = $genre->animes()->where('is_published', true)->latest()->paginate(24); return view('frontend.genre', compact('genre', 'animes')); } }