id === $user->id) { return response()->json(['error' => 'Kendinizi takip edemezsiniz.'], 422); } $existing = UserFollow::where('follower_id', $me->id) ->where('following_id', $user->id) ->first(); if ($existing) { $existing->delete(); $following = false; } else { UserFollow::create(['follower_id' => $me->id, 'following_id' => $user->id]); $following = true; } return response()->json([ 'following' => $following, 'followers_count' => UserFollow::where('following_id', $user->id)->count(), ]); } public function card(User $user) { $me = Auth::user(); $isFollowing = $me ? UserFollow::where('follower_id', $me->id)->where('following_id', $user->id)->exists() : false; return response()->json([ 'id' => $user->id, 'name' => $user->name, 'username' => $user->username, 'avatar' => $user->avatar ? MediaUrl::fromStoragePath($user->avatar) : null, 'followers' => UserFollow::where('following_id', $user->id)->count(), 'following' => UserFollow::where('follower_id', $user->id)->count(), 'is_following' => $isFollowing, 'profile_url' => route('user.profile', $user), 'follow_url' => ($me && $me->id !== $user->id) ? route('user.follow', $user) : null, 'msg_url' => ($me && $me->id !== $user->id) ? route('messages.start', $user) : null, 'is_me' => $me && $me->id === $user->id, ]); } public function compatibility(User $user) { $me = Auth::user(); if (!$me) return response()->json(['score' => 0]); return response()->json([ 'score' => $me->compatibilityWith($user), ]); } // ───────────────────────────────────────────────────────── // NicoNico — Timestamp Yorumları // ───────────────────────────────────────────────────────── public function timestampComments(Episode $episode) { $comments = EpisodeTimestampComment::with('user:id,name,username') ->where('episode_id', $episode->id) ->where('is_hidden', false) ->orderBy('timestamp_sec') ->get() ->map(fn($c) => [ 'id' => $c->id, 'user_id' => $c->user_id, 'timestamp_sec' => $c->timestamp_sec, 'body' => $c->body, 'color' => $c->color, 'username' => $c->user?->username ?? 'misafir', 'created_at' => $c->created_at, ]); return response()->json(['comments' => $comments]); } public function timestampCommentStore(Request $request, Episode $episode) { $data = $request->validate([ 'timestamp_sec' => 'required|integer|min:0|max:86400', 'body' => 'required|string|max:100', 'color' => 'nullable|regex:/^#[0-9a-fA-F]{6}$/', ]); $me = Auth::user(); // Flood koruması: aynı kullanıcı 5 saniye içinde 2+ yorum atmasın $recent = EpisodeTimestampComment::where('user_id', $me->id) ->where('episode_id', $episode->id) ->where('created_at', '>=', now()->subSeconds(5)) ->count(); if ($recent >= 2) { return response()->json(['error' => 'Çok hızlı yorum yapıyorsunuz.'], 429); } $comment = EpisodeTimestampComment::create([ 'episode_id' => $episode->id, 'user_id' => $me->id, 'timestamp_sec' => $data['timestamp_sec'], 'body' => $data['body'], 'color' => $data['color'] ?? '#ffffff', ]); return response()->json(['ok' => true, 'id' => $comment->id]); } // ───────────────────────────────────────────────────────── // Tahmin Oyunu // ───────────────────────────────────────────────────────── public function predictions(Episode $episode) { $me = Auth::id(); $predictions = EpisodePrediction::with('user:id,name,username') ->where('episode_id', $episode->id) ->orderByDesc('vote_count') ->get() ->map(fn($p) => [ 'id' => $p->id, 'body' => $p->body, 'is_correct' => $p->is_correct, 'vote_count' => $p->vote_count, 'username' => $p->user?->username, 'is_mine' => $me && $p->user_id === $me, 'voted' => $me ? PredictionVote::where('prediction_id', $p->id)->where('user_id', $me)->exists() : false, 'created_at' => $p->created_at->diffForHumans(), ]); $myPrediction = $me ? EpisodePrediction::where('episode_id', $episode->id)->where('user_id', $me)->first()?->id : null; return response()->json([ 'predictions' => $predictions, 'my_prediction' => $myPrediction, ]); } public function predictionStore(Request $request, Episode $episode) { $me = Auth::user(); $data = $request->validate([ 'body' => 'required|string|min:5|max:280', ]); $existing = EpisodePrediction::where('episode_id', $episode->id) ->where('user_id', $me->id) ->first(); if ($existing) { return response()->json(['error' => 'Bu bölüm için zaten bir tahmininiz var.'], 422); } $prediction = EpisodePrediction::create([ 'episode_id' => $episode->id, 'user_id' => $me->id, 'body' => $data['body'], ]); return response()->json(['ok' => true, 'id' => $prediction->id]); } public function predictionVote(Request $request, EpisodePrediction $prediction) { $me = Auth::user(); $existing = PredictionVote::where('prediction_id', $prediction->id) ->where('user_id', $me->id) ->first(); if ($existing) { $existing->delete(); $prediction->decrement('vote_count'); return response()->json(['voted' => false, 'vote_count' => $prediction->fresh()->vote_count]); } PredictionVote::create(['prediction_id' => $prediction->id, 'user_id' => $me->id]); $prediction->increment('vote_count'); return response()->json(['voted' => true, 'vote_count' => $prediction->fresh()->vote_count]); } // ───────────────────────────────────────────────────────── // Watch Party // ───────────────────────────────────────────────────────── public function partyCreate(Request $request) { $me = Auth::user(); $data = $request->validate([ 'episode_id' => 'required|exists:episodes,id', 'is_private' => 'boolean', 'password' => 'nullable|string|max:30', 'max_members'=> 'nullable|integer|min:2|max:20', ]); // Kullanıcının zaten aktif bir odası varsa sil WatchParty::where('host_user_id', $me->id)->delete(); $party = WatchParty::create([ 'room_code' => WatchParty::generateCode(), 'host_user_id' => $me->id, 'episode_id' => $data['episode_id'], 'is_private' => $data['is_private'] ?? false, 'password' => isset($data['password']) ? Hash::make($data['password']) : null, 'max_members' => $data['max_members'] ?? 10, ]); WatchPartyMember::create([ 'party_id' => $party->id, 'user_id' => $me->id, ]); return response()->json([ 'ok' => true, 'room_code' => $party->room_code, 'party_url' => route('watch.party', $party->room_code), ]); } public function partyJoin(Request $request, string $roomCode) { $party = WatchParty::where('room_code', $roomCode)->firstOrFail(); $me = Auth::user(); // Şifre kontrolü if ($party->is_private && $party->password) { $pw = $request->input('password', ''); if (!Hash::check($pw, $party->password)) { return response()->json(['error' => 'Yanlış şifre.'], 403); } } // Kapasite $activeCount = $party->activeMembers()->count(); if ($activeCount >= $party->max_members) { return response()->json(['error' => 'Oda dolu.'], 403); } WatchPartyMember::updateOrCreate( ['party_id' => $party->id, 'user_id' => $me->id], ['last_ping' => now()] ); return response()->json([ 'ok' => true, 'current_sec' => $party->current_sec, 'is_playing' => $party->is_playing, 'host_id' => $party->host_user_id, 'members' => $this->partyMemberList($party), ]); } public function partySync(Request $request, string $roomCode) { $party = WatchParty::where('room_code', $roomCode)->firstOrFail(); $me = Auth::user(); // Sadece host senkron durumu güncelleyebilir if ($party->host_user_id === $me->id) { $data = $request->validate([ 'current_sec' => 'required|integer|min:0', 'is_playing' => 'required|boolean', ]); $party->update([ 'current_sec' => $data['current_sec'], 'is_playing' => $data['is_playing'], ]); } // Herkes ping atar WatchPartyMember::where('party_id', $party->id) ->where('user_id', $me->id) ->update(['last_ping' => now()]); return response()->json([ 'current_sec' => $party->fresh()->current_sec, 'is_playing' => $party->fresh()->is_playing, 'members' => $this->partyMemberList($party), ]); } public function partyLeave(string $roomCode) { $party = WatchParty::where('room_code', $roomCode)->firstOrFail(); $me = Auth::user(); WatchPartyMember::where('party_id', $party->id)->where('user_id', $me->id)->delete(); if ($party->host_user_id === $me->id) { $party->delete(); return response()->json(['ok' => true, 'dissolved' => true]); } return response()->json(['ok' => true, 'dissolved' => false]); } public function partyShow(string $roomCode) { $party = WatchParty::with(['episode.anime', 'host'])->where('room_code', $roomCode)->firstOrFail(); return view('frontend.watch-party', compact('party')); } private function partyMemberList(WatchParty $party): array { return $party->activeMembers()->with('user:id,name,username')->get() ->map(fn($m) => [ 'id' => $m->user_id, 'name' => $m->user?->name, 'username' => $m->user?->username, 'is_host' => $m->user_id === $party->host_user_id, ])->toArray(); } // ───────────────────────────────────────────────────────── // İlk Kez İzleyenler // ───────────────────────────────────────────────────────── public function firstWatchRegister(Request $request, Episode $episode) { $me = Auth::user(); $sessionId = $request->header('X-Session-ID') ?? session()->getId(); FirstWatchSession::updateOrCreate( [ 'episode_id' => $episode->id, 'user_id' => $me?->id, 'session_id' => $me ? null : $sessionId, ], [ 'is_first_time' => (bool)$request->input('is_first_time', true), 'last_seen' => now(), ] ); $count = FirstWatchSession::where('episode_id', $episode->id) ->where('is_first_time', true) ->where('last_seen', '>=', now()->subMinutes(10)) ->count(); return response()->json(['ok' => true, 'first_watch_count' => $count]); } public function firstWatchCount(Episode $episode) { $count = FirstWatchSession::where('episode_id', $episode->id) ->where('is_first_time', true) ->where('last_seen', '>=', now()->subMinutes(10)) ->count(); return response()->json(['count' => $count]); } // ───────────────────────────────────────────────────────── // Ruh Hali Motoru // ───────────────────────────────────────────────────────── private static array $moodGenres = [ 'sad' => ['Drama', 'Romantizm'], 'funny' => ['Komedi', 'Slice of Life'], 'hype' => ['Aksiyon', 'Shounen', 'Spor'], 'think' => ['Bilim Kurgu', 'Gerilim', 'Supernatural'], 'romance' => ['Romantizm', 'Shoujo'], 'scary' => ['Korku', 'Supernatural', 'Gerilim'], ]; public function moodRecommend(Request $request) { $mood = $request->validate(['mood' => 'required|in:sad,funny,hype,think,romance,scary'])['mood']; $genres = self::$moodGenres[$mood] ?? []; $animes = Anime::whereHas('genres', fn($q) => $q->whereIn('name', $genres)) ->where('is_published', true) ->inRandomOrder() ->limit(6) ->get(['id', 'title', 'cover_image', 'slug', 'rating']); return response()->json([ 'animes' => $animes->map(fn($a) => [ 'id' => $a->id, 'title' => $a->title, 'cover' => $a->cover_image ? \App\Support\MediaUrl::fromStoragePath($a->cover_image) : null, 'url' => route('anime.show', $a->slug), 'rating'=> $a->rating, ]), ]); } // ───────────────────────────────────────────────────────── // Zaman Kapsülü // ───────────────────────────────────────────────────────── public function capsuleStore(Request $request) { $me = Auth::user(); $data = $request->validate([ 'anime_id' => 'required|exists:animes,id', 'message' => 'required|string|min:5|max:1000', 'unlock_at' => 'required|date|after:' . now()->addDays(30)->toDateString(), ]); $data['user_id'] = $me->id; $capsule = TimeCapsule::create($data); return response()->json(['ok' => true, 'id' => $capsule->id]); } public function capsuleIndex() { $capsules = TimeCapsule::with('anime:id,title,slug,cover_image') ->where('user_id', Auth::id()) ->orderBy('unlock_at') ->get() ->map(fn($c) => [ 'id' => $c->id, 'anime' => $c->anime?->title, 'anime_url' => $c->anime ? route('anime.show', $c->anime->slug) : null, 'cover' => $c->anime?->cover_image ? MediaUrl::fromStoragePath($c->anime->cover_image) : null, 'unlock_at' => $c->unlock_at->format('d.m.Y'), 'unlocked' => $c->isUnlocked(), 'opened' => $c->isOpened(), 'message' => $c->isOpened() || $c->isUnlocked() ? $c->message : null, 'created_at' => $c->created_at->format('d.m.Y'), ]); return view('frontend.capsules', compact('capsules')); } public function capsuleOpen(TimeCapsule $capsule) { if ($capsule->user_id !== Auth::id()) { return response()->json(['error' => 'Yetkisiz.'], 403); } if (!$capsule->isUnlocked()) { return response()->json(['error' => 'Kapsül henüz açılamaz.'], 422); } $capsule->update(['opened_at' => now()]); return response()->json(['ok' => true, 'message' => $capsule->message]); } // ───────────────────────────────────────────────────────── // Spoiler Kilitli Kutu // ───────────────────────────────────────────────────────── public function spoilerBoxes(Episode $episode) { $me = Auth::id(); $boxes = SpoilerBox::with('user:id,name,username') ->where('episode_id', $episode->id) ->orderByDesc('likes') ->orderByDesc('created_at') ->get() ->map(fn($b) => [ 'id' => $b->id, 'body' => $b->body, 'is_spoiler' => $b->is_spoiler, 'spoiler_score' => $b->spoiler_score, 'likes' => $b->likes, 'username' => $b->user?->username, 'name' => $b->user?->name, 'is_mine' => $me && $b->user_id === $me, 'liked' => $me ? SpoilerBoxLike::where('box_id', $b->id)->where('user_id', $me)->exists() : false, 'created_at' => $b->created_at->diffForHumans(), ]); return response()->json(['boxes' => $boxes]); } public function spoilerBoxStore(Request $request, Episode $episode) { $me = Auth::user(); $data = $request->validate([ 'body' => 'required|string|min:3|max:600', ]); // AI spoiler tespiti $isSpoiler = false; $spoilerScore = 0; $ai = new DeepSeekService(); if ($ai->isConfigured()) { $prompt = "Aşağıdaki metin bir anime bölümü hakkında yazılmış. Bu metin spoiler içeriyor mu? " . "Sadece JSON döndür: {\"is_spoiler\": true/false, \"score\": 0-100}\n\nMetin: " . $data['body']; try { $raw = $ai->checkSpoiler($data['body']); if ($raw) { $isSpoiler = $raw['is_spoiler'] ?? false; $spoilerScore = $raw['score'] ?? 0; } } catch (\Throwable $e) {} } $box = SpoilerBox::create([ 'episode_id' => $episode->id, 'user_id' => $me->id, 'body' => $data['body'], 'is_spoiler' => $isSpoiler, 'spoiler_score' => $spoilerScore, ]); return response()->json([ 'ok' => true, 'id' => $box->id, 'is_spoiler' => $isSpoiler, ]); } public function spoilerBoxLike(SpoilerBox $box) { $me = Auth::id(); $existing = SpoilerBoxLike::where('box_id', $box->id)->where('user_id', $me)->first(); if ($existing) { $existing->delete(); $box->decrement('likes'); return response()->json(['liked' => false, 'likes' => $box->fresh()->likes]); } SpoilerBoxLike::create(['box_id' => $box->id, 'user_id' => $me, 'created_at' => now()]); $box->increment('likes'); return response()->json(['liked' => true, 'likes' => $box->fresh()->likes]); } }