Files
animexe/app/Http/Controllers/Api/SocialApiController.php
T
2026-07-14 00:01:48 +03:00

463 lines
17 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Anime;
use App\Models\Episode;
use App\Models\EpisodePrediction;
use App\Models\EpisodeTimestampComment;
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 SocialApiController extends Controller
{
// ── 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',
]);
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();
$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(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',
]);
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' => $this->partyData($party),
]);
}
public function partyJoin(Request $request, string $roomCode)
{
$party = WatchParty::where('room_code', $roomCode)->firstOrFail();
$me = Auth::user();
if ($party->is_private && $party->password) {
if (!Hash::check($request->input('password', ''), $party->password)) {
return response()->json(['error' => 'Yanlış şifre.'], 403);
}
}
if ($party->activeMembers()->count() >= $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,
'party' => $this->partyData($party),
]);
}
public function partySync(Request $request, string $roomCode)
{
$party = WatchParty::where('room_code', $roomCode)->firstOrFail();
$me = Auth::user();
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'],
]);
}
WatchPartyMember::where('party_id', $party->id)->where('user_id', $me->id)
->update(['last_ping' => now()]);
$fresh = $party->fresh();
return response()->json([
'current_sec' => $fresh->current_sec,
'is_playing' => $fresh->is_playing,
'members' => $this->memberList($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 partyInfo(string $roomCode)
{
$party = WatchParty::with(['episode.anime', 'episode.season'])
->where('room_code', $roomCode)->firstOrFail();
return response()->json(['party' => $this->partyData($party)]);
}
private function partyData(WatchParty $party): array
{
$party->loadMissing(['episode.anime', 'episode.season']);
return [
'room_code' => $party->room_code,
'host_id' => $party->host_user_id,
'episode_id' => $party->episode_id,
'current_sec' => $party->current_sec,
'is_playing' => $party->is_playing,
'is_private' => $party->is_private,
'max_members' => $party->max_members,
'members' => $this->memberList($party),
'anime_title' => $party->episode?->anime?->title,
'anime_slug' => $party->episode?->anime?->slug,
'episode_num' => $party->episode?->episode_number,
'season_num' => $party->episode?->season?->season_number ?? 1,
];
}
private function memberList(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();
}
// ── Spoiler Kutular ──────────────────────────────────────────────────────
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,
'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']);
$isSpoiler = false;
$spoilerScore = 0;
$ai = new DeepSeekService();
if ($ai->isConfigured()) {
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]);
}
// ── Zaman Kapsülü ────────────────────────────────────────────────────────
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_title' => $c->anime?->title,
'anime_slug' => $c->anime?->slug,
'cover' => $c->anime?->cover_image ? MediaUrl::fromStoragePath($c->anime->cover_image) : null,
'unlock_at' => $c->unlock_at->toIso8601String(),
'unlocked' => $c->isUnlocked(),
'opened' => $c->isOpened(),
'message' => ($c->isOpened() || $c->isUnlocked()) ? $c->message : null,
'created_at' => $c->created_at->toIso8601String(),
]);
return response()->json(['capsules' => $capsules]);
}
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 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]);
}
// ── 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,
'slug' => $a->slug,
'cover' => $a->cover_image ? MediaUrl::fromStoragePath($a->cover_image) : null,
'rating' => $a->rating,
]),
]);
}
// ── 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 = \App\Models\UserFollow::where('follower_id', $me->id)
->where('following_id', $user->id)->first();
if ($existing) {
$existing->delete();
$following = false;
} else {
\App\Models\UserFollow::create(['follower_id' => $me->id, 'following_id' => $user->id]);
$following = true;
}
return response()->json([
'following' => $following,
'followers_count' => \App\Models\UserFollow::where('following_id', $user->id)->count(),
]);
}
}