254 lines
9.8 KiB
PHP
254 lines
9.8 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers\Frontend;
|
||
|
||
use App\Http\Controllers\Controller;
|
||
use App\Models\Anime;
|
||
use App\Models\AnimeSwipe;
|
||
use App\Models\Genre;
|
||
use App\Models\Watchlist;
|
||
use App\Services\DeepSeekService;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Support\Facades\Cache;
|
||
use Illuminate\Support\Str;
|
||
|
||
class DiscoverController extends Controller
|
||
{
|
||
public function index()
|
||
{
|
||
$genres = Genre::where('is_active', true)->orderBy('name')->get(['id', 'name', 'slug']);
|
||
return view('frontend.discover', compact('genres'));
|
||
}
|
||
|
||
public function cards(Request $request)
|
||
{
|
||
$genreSlug = $request->input('genre', '');
|
||
$type = $request->input('type', '');
|
||
$limit = min((int) $request->input('limit', 10), 10);
|
||
|
||
// Auth state'i al
|
||
$isAuth = auth()->check();
|
||
$uid = $isAuth ? auth()->id() : null;
|
||
|
||
try {
|
||
$idQuery = Anime::where('is_published', true)->select('id');
|
||
|
||
if ($genreSlug) {
|
||
$idQuery->whereHas('genres', fn($q) => $q->where('slug', $genreSlug));
|
||
}
|
||
if ($type) {
|
||
$idQuery->where('type', $type);
|
||
}
|
||
|
||
if ($isAuth && $uid) {
|
||
$exclude = AnimeSwipe::where('user_id', $uid)->pluck('anime_id')
|
||
->merge(Watchlist::where('user_id', $uid)->pluck('anime_id'))
|
||
->unique();
|
||
if ($exclude->isNotEmpty()) {
|
||
$idQuery->whereNotIn('id', $exclude);
|
||
}
|
||
}
|
||
|
||
$ids = $idQuery->pluck('id');
|
||
if ($ids->isEmpty()) {
|
||
return response()->json(['cards' => [], 'has_more' => false]);
|
||
}
|
||
|
||
$randomIds = $ids->shuffle()->take($limit);
|
||
|
||
$animes = Anime::whereIn('id', $randomIds)
|
||
->with('genres:id,name')
|
||
->get()
|
||
->shuffle();
|
||
|
||
$cards = $animes->map(function (Anime $anime) {
|
||
$hook = $anime->discovery_hook
|
||
?: ($anime->description ? Str::limit(strip_tags($anime->description), 130) : null);
|
||
|
||
return [
|
||
'id' => $anime->id,
|
||
'slug' => $anime->slug,
|
||
'title' => $anime->title,
|
||
'cover_url' => $anime->coverUrl,
|
||
'banner_url' => $anime->bannerUrl,
|
||
'rating' => $anime->rating ? number_format($anime->rating, 1) : null,
|
||
'year' => $anime->release_year,
|
||
'type' => $anime->type,
|
||
'status' => $anime->status,
|
||
'episode_count' => $anime->episode_count,
|
||
'genres' => $anime->genres->take(3)->pluck('name')->values(),
|
||
'hook' => $hook,
|
||
'description' => $anime->description ? Str::limit(strip_tags($anime->description), 420) : null,
|
||
];
|
||
});
|
||
|
||
return response()->json([
|
||
'cards' => $cards,
|
||
'has_more' => $animes->count() === $limit,
|
||
]);
|
||
|
||
} catch (\Exception $e) {
|
||
\Log::error('Discover cards error: ' . $e->getMessage());
|
||
return response()->json(['cards' => [], 'has_more' => false, 'error' => true]);
|
||
}
|
||
}
|
||
|
||
public function swipe(Request $request)
|
||
{
|
||
$data = $request->validate([
|
||
'anime_id' => 'required|integer|exists:animes,id',
|
||
'direction' => 'required|in:like,skip',
|
||
]);
|
||
|
||
if (auth()->check()) {
|
||
$uid = auth()->id();
|
||
AnimeSwipe::updateOrCreate(
|
||
['user_id' => $uid, 'anime_id' => $data['anime_id']],
|
||
['direction' => $data['direction']]
|
||
);
|
||
|
||
if ($data['direction'] === 'like') {
|
||
Watchlist::updateOrCreate(
|
||
['user_id' => $uid, 'anime_id' => $data['anime_id']],
|
||
['status' => 'plan']
|
||
);
|
||
}
|
||
} else {
|
||
$seen = session('guest_swipes', []);
|
||
$seen[] = $data['anime_id'];
|
||
session(['guest_swipes' => array_unique(array_slice($seen, -150))]);
|
||
}
|
||
|
||
return response()->json(['ok' => true]);
|
||
}
|
||
|
||
public function reset()
|
||
{
|
||
if (auth()->check()) {
|
||
AnimeSwipe::where('user_id', auth()->id())->delete();
|
||
} else {
|
||
session()->forget('guest_swipes');
|
||
}
|
||
return response()->json(['ok' => true]);
|
||
}
|
||
|
||
public function results(Request $request)
|
||
{
|
||
// Beğenilen animeler
|
||
if (auth()->check()) {
|
||
$uid = auth()->id();
|
||
$swipes = AnimeSwipe::where('user_id', $uid)
|
||
->with('anime:id,title,slug,cover_image,rating,release_year,type')
|
||
->orderByDesc('created_at')
|
||
->get()->filter(fn($s) => $s->anime);
|
||
|
||
$likedAnimes = $swipes->where('direction', 'like')
|
||
->map(fn($s) => $s->anime)
|
||
->values();
|
||
|
||
$allSwipedIds = $swipes->pluck('anime_id');
|
||
$likeCount = $swipes->where('direction', 'like')->count();
|
||
$skipCount = $swipes->where('direction', 'skip')->count();
|
||
} else {
|
||
$seen = session('guest_swipes', []);
|
||
$likedAnimes = collect();
|
||
$allSwipedIds= collect($seen);
|
||
$likeCount = 0;
|
||
$skipCount = count($seen);
|
||
}
|
||
|
||
// AI önerileri — beğenilen animelerin türlerine benzer, henüz görülmemiş
|
||
$recommendations = collect();
|
||
if ($likedAnimes->isNotEmpty()) {
|
||
$ai = app(DeepSeekService::class);
|
||
|
||
// Beğenilen animelerin genre'larını topla
|
||
$likedWithGenres = Anime::whereIn('id', $likedAnimes->pluck('id'))
|
||
->with('genres:id,name')
|
||
->get();
|
||
$genreIds = $likedWithGenres->flatMap(fn($a) => $a->genres->pluck('id'))->unique();
|
||
|
||
// Benzer ama henüz görülmemiş animeler al — ID shuffle ile ORDER BY RAND() önlenir
|
||
$candidateIds = Anime::where('is_published', true)
|
||
->whereNotIn('id', $allSwipedIds)
|
||
->whereHas('genres', fn($q) => $q->whereIn('id', $genreIds))
|
||
->pluck('id')
|
||
->shuffle()
|
||
->take(30);
|
||
|
||
$candidateAnimes = Anime::whereIn('id', $candidateIds)
|
||
->with('genres:id,name')
|
||
->withCount('episodes')
|
||
->get()
|
||
->shuffle();
|
||
|
||
if ($ai->isConfigured() && $candidateAnimes->isNotEmpty()) {
|
||
$likedTitles = $likedAnimes->pluck('title')->take(5)->join(', ');
|
||
$preferences = "Kullanıcının beğendiği animeler: {$likedTitles}. Bunlara benzer, aynı türde ya da aynı atmosferde animeler öner.";
|
||
|
||
$candidateData = $candidateAnimes->map(fn($a) => [
|
||
'id' => $a->id,
|
||
'title' => $a->title,
|
||
'type' => $a->type,
|
||
'release_year' => $a->release_year,
|
||
'rating' => $a->rating,
|
||
'genres' => $a->genres->map(fn($g) => ['name' => $g->name])->toArray(),
|
||
])->values()->toArray();
|
||
|
||
$aiRecs = Cache::remember(
|
||
'dsc_recs_' . md5($likedAnimes->pluck('id')->sort()->join(',')),
|
||
60 * 60 * 6,
|
||
fn() => $ai->recommend($preferences, $candidateData)
|
||
);
|
||
|
||
if ($aiRecs) {
|
||
$recIds = collect($aiRecs)->pluck('id')->map('intval');
|
||
$recAnimes = $candidateAnimes->whereIn('id', $recIds)->keyBy('id');
|
||
|
||
$recommendations = collect($aiRecs)->take(6)->map(function ($rec) use ($recAnimes) {
|
||
$anime = $recAnimes->get((int)$rec['id']);
|
||
if (!$anime) return null;
|
||
return [
|
||
'id' => $anime->id,
|
||
'slug' => $anime->slug,
|
||
'title' => $anime->title,
|
||
'cover_url' => $anime->coverUrl,
|
||
'rating' => $anime->rating ? number_format($anime->rating, 1) : null,
|
||
'year' => $anime->release_year,
|
||
'genres' => $anime->genres->take(2)->pluck('name')->values(),
|
||
'reason' => $rec['reason'] ?? null,
|
||
];
|
||
})->filter()->values();
|
||
}
|
||
}
|
||
|
||
// AI yoksa genre-based fallback
|
||
if ($recommendations->isEmpty()) {
|
||
$recommendations = $candidateAnimes->take(6)->map(fn($a) => [
|
||
'id' => $a->id,
|
||
'slug' => $a->slug,
|
||
'title' => $a->title,
|
||
'cover_url' => $a->coverUrl,
|
||
'rating' => $a->rating ? number_format($a->rating, 1) : null,
|
||
'year' => $a->release_year,
|
||
'genres' => $a->genres->take(2)->pluck('name')->values(),
|
||
'reason' => null,
|
||
])->values();
|
||
}
|
||
}
|
||
|
||
return response()->json([
|
||
'liked' => $likedAnimes->map(fn($a) => [
|
||
'id' => $a->id,
|
||
'slug' => $a->slug,
|
||
'title' => $a->title,
|
||
'cover_url' => $a->coverUrl,
|
||
])->values(),
|
||
'recommendations' => $recommendations,
|
||
'like_count' => $likeCount,
|
||
'skip_count' => $skipCount,
|
||
'is_auth' => auth()->check(),
|
||
]);
|
||
}
|
||
}
|