448 lines
20 KiB
PHP
448 lines
20 KiB
PHP
<?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'));
|
||
}
|
||
}
|