144 lines
4.7 KiB
PHP
144 lines
4.7 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Episode;
|
|
use App\Models\VideoEngagement;
|
|
use App\Models\Watched;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\RateLimiter;
|
|
|
|
class StreamApiController extends Controller
|
|
{
|
|
/**
|
|
* Get Data used by the Video Player.
|
|
*/
|
|
public function getStream(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'episode_id' => 'required',
|
|
]);
|
|
|
|
$episode = Episode::where('id', $request->input('episode_id'))->firstOrFail();
|
|
|
|
$subtitles = $episode->subtitles
|
|
->mapWithKeys(fn ($sub) => [$sub->subtitle->slug => $sub->subtitle->name])
|
|
->toArray();
|
|
|
|
return response()->json([
|
|
'title' => $episode->title.' - '.$episode->episode,
|
|
'poster' => $episode->gallery()->first()->image_url,
|
|
'interpolated' => $episode->interpolated,
|
|
'interpolated_uhd' => $episode->interpolated_uhd,
|
|
'stream_url' => $episode->dmca_takedown ? 'stuff/dmca' : $episode->url,
|
|
'stream_domains' => config('hstream.stream_domain'),
|
|
'asia_stream_domains' => config('hstream.asia_stream_domain'),
|
|
'extra_subtitles' => $subtitles,
|
|
], 200);
|
|
}
|
|
|
|
/**
|
|
* Track that the authenticated user has watched the episode.
|
|
* Called client-side after 10 seconds of playback.
|
|
*/
|
|
public function trackWatched(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'episode_id' => 'required|integer|exists:episodes,id',
|
|
]);
|
|
|
|
$user = Auth::user();
|
|
if (! $user) {
|
|
return response()->json(['watched' => false], 401);
|
|
}
|
|
|
|
$episodeId = $request->input('episode_id');
|
|
|
|
// 1-hour cooldown to prevent duplicate entries
|
|
$time = Carbon::now()->subHour(1);
|
|
$alreadyWatched = Watched::where('user_id', $user->id)
|
|
->where('episode_id', $episodeId)
|
|
->where('created_at', '>=', $time)
|
|
->exists();
|
|
|
|
if (! $alreadyWatched) {
|
|
Watched::create(['user_id' => $user->id, 'episode_id' => $episodeId]);
|
|
cache()->forget('user'.$user->id.'watched'.$episodeId);
|
|
}
|
|
|
|
return response()->json(['watched' => true], 200);
|
|
}
|
|
|
|
/**
|
|
* Track engagement segments that the authenticated user has watched.
|
|
* Called periodically client-side while the video is playing.
|
|
* Segments are 10-second chunks; segment 0 (0-10s) is excluded.
|
|
*/
|
|
public function trackEngagement(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'episode_id' => 'required|integer|exists:episodes,id',
|
|
'segments' => 'required|array',
|
|
'segments.*' => 'integer|min:1',
|
|
]);
|
|
|
|
$user = Auth::user();
|
|
if (! $user) {
|
|
return response()->json(['tracked' => 0], 401);
|
|
}
|
|
|
|
$rateLimitKey = 'engagement:'.$user->id;
|
|
if (RateLimiter::tooManyAttempts($rateLimitKey, 10)) {
|
|
$seconds = RateLimiter::availableIn($rateLimitKey);
|
|
|
|
return response()->json([
|
|
'tracked' => 0,
|
|
'message' => 'Rate limit exceeded. Try again in '.$seconds.' seconds.',
|
|
], 429);
|
|
}
|
|
RateLimiter::hit($rateLimitKey, 60);
|
|
|
|
$episodeId = $request->input('episode_id');
|
|
$segments = $request->input('segments');
|
|
$tracked = 0;
|
|
|
|
foreach ($segments as $segment) {
|
|
// upsert: insert if not exists, otherwise ignore (unique constraint prevents dupes)
|
|
VideoEngagement::firstOrCreate([
|
|
'episode_id' => $episodeId,
|
|
'user_id' => $user->id,
|
|
'segment' => $segment,
|
|
]);
|
|
$tracked++;
|
|
}
|
|
|
|
return response()->json(['tracked' => $tracked], 200);
|
|
}
|
|
|
|
/**
|
|
* Get engagement heatmap data for an episode.
|
|
* Returns watch counts per segment, aggregated across all users.
|
|
*/
|
|
public function getEngagement(int $episodeId): JsonResponse
|
|
{
|
|
$episode = Episode::findOrFail($episodeId);
|
|
|
|
$data = cache()->remember(
|
|
"engagement:{$episodeId}",
|
|
600, // 10-minute cache
|
|
fn () => VideoEngagement::where('episode_id', $episodeId)
|
|
->select('segment', DB::raw('count(*) as watch_count'))
|
|
->groupBy('segment')
|
|
->pluck('watch_count', 'segment')
|
|
->toArray()
|
|
);
|
|
|
|
return response()->json($data, 200);
|
|
}
|
|
}
|