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

448 lines
20 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\Frontend;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Admin\TrendingController;
use App\Models\Anime;
use App\Models\Banner;
use App\Models\Episode;
use App\Models\Genre;
use App\Models\ContinueWatching;
use App\Models\User;
use App\Support\MediaUrl;
use Illuminate\Support\Facades\Cookie;
use Illuminate\Support\Facades\DB;
class HomeController extends Controller
{
public function index()
{
// ── Cache key: 15 dakikada bir rotasyon ──────────────────────────────
$rotationSlot = (int) floor(now()->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'));
}
}