Initial commit: Animexe Laravel platform

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 00:01:48 +03:00
co-authored by Claude Opus 4.8
commit a63515cfc6
366 changed files with 74773 additions and 0 deletions
@@ -0,0 +1,253 @@
<?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(),
]);
}
}