Initial commit: Animexe Laravel platform
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,569 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Frontend;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Anime;
|
||||
use App\Models\Episode;
|
||||
use App\Models\EpisodePrediction;
|
||||
use App\Models\EpisodeTimestampComment;
|
||||
use App\Models\FirstWatchSession;
|
||||
use App\Models\PredictionVote;
|
||||
use App\Models\SpoilerBox;
|
||||
use App\Models\SpoilerBoxLike;
|
||||
use App\Models\TimeCapsule;
|
||||
use App\Models\User;
|
||||
use App\Models\UserFollow;
|
||||
use App\Models\WatchParty;
|
||||
use App\Models\WatchPartyMember;
|
||||
use App\Services\DeepSeekService;
|
||||
use App\Support\MediaUrl;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class SocialController extends Controller
|
||||
{
|
||||
// ─────────────────────────────────────────────────────────
|
||||
// Kullanıcı takip
|
||||
// ─────────────────────────────────────────────────────────
|
||||
|
||||
public function followToggle(User $user)
|
||||
{
|
||||
$me = Auth::user();
|
||||
|
||||
if ($me->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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user