From 9ad842dcd50ec5bc01b27cdc9aab8918a17816e8 Mon Sep 17 00:00:00 2001 From: ghassan Date: Tue, 3 Mar 2026 21:30:44 +0300 Subject: [PATCH] Add trending videos page with YouTube-style algorithm - Trending algorithm based on: - Recent views (last 48h): 70% weight - View velocity (views/hour): 15% weight - Recency bonus: 10% weight - Like count: 5% weight - Excludes videos older than 10 days - Filter options: Today/This Week/This Month - Added route, controller method, and view - Updated nav to link to trending page --- app/Http/Controllers/VideoController.php | 43 +++++ app/Models/Video.php | 65 +++++++ resources/views/layouts/app.blade.php | 4 +- resources/views/videos/trending.blade.php | 216 ++++++++++++++++++++++ routes/web.php | 1 + 5 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 resources/views/videos/trending.blade.php diff --git a/app/Http/Controllers/VideoController.php b/app/Http/Controllers/VideoController.php index 4f8ac72..406c6e1 100644 --- a/app/Http/Controllers/VideoController.php +++ b/app/Http/Controllers/VideoController.php @@ -395,4 +395,47 @@ class VideoController extends Controller return response()->download($path, $filename); } + + // Trending videos page + public function trending(Request $request) + { + $hours = $request->get('hours', 48); // Default: 48 hours + $limit = $request->get('limit', 50); + + // Validate parameters + $hours = min(max($hours, 24), 168); // Between 24h and 7 days + $limit = min(max($limit, 10), 100); + + // Get trending videos using the scope with raw score calculation + $trendingVideos = \DB::table('videos') + ->select('videos.*', + \DB::raw('( + (SELECT COUNT(*) FROM video_views vv WHERE vv.video_id = videos.id AND vv.watched_at >= DATE_SUB(NOW(), INTERVAL ' . $hours . ' HOUR)) * 0.70 + + (SELECT COUNT(*) FROM video_views vv WHERE vv.video_id = videos.id AND vv.watched_at >= DATE_SUB(NOW(), INTERVAL ' . $hours . ' HOUR)) / ' . $hours . ' * 100 * 0.15 + + GREATEST(0, 1 - TIMESTAMPDIFF(HOUR, videos.created_at, NOW()) / 240) * 50 * 0.10 + + (SELECT COUNT(*) FROM video_likes vl WHERE vl.video_id = videos.id) * 0.1 * 0.05 + ) as trending_score') + ) + ->where('visibility', 'public') + ->where('status', 'ready') + ->where('created_at', '>=', now()->subDays(10)) + ->having('trending_score', '>', 0) + ->orderByDesc('trending_score') + ->limit($limit) + ->get(); + + // Load user relationship + $trendingVideos = $trendingVideos->map(function ($video) { + $video->user = \App\Models\User::find($video->user_id); + $video->view_count = \DB::table('video_views')->where('video_id', $video->id)->count(); + $video->like_count = \DB::table('video_likes')->where('video_id', $video->id)->count(); + return $video; + }); + + return view('videos.trending', [ + 'videos' => $trendingVideos, + 'hours' => $hours, + 'limit' => $limit, + ]); + } } diff --git a/app/Models/Video.php b/app/Models/Video.php index b2713fb..c543a25 100644 --- a/app/Models/Video.php +++ b/app/Models/Video.php @@ -186,3 +186,68 @@ class Video extends Model return $this->comments()->count(); } } + + // Get recent views count (within hours) + public function getRecentViews( = 48) + { + return \DB::table('video_views') + ->where('video_id', ->id) + ->where('watched_at', '>=', now()->subHours()) + ->count(); + } + + // Get views in last 24 hours (for velocity calculation) + public function getViewsLast24Hours() + { + return ->getRecentViews(24); + } + + // Calculate trending score (YouTube-style algorithm) + public function getTrendingScore( = 48) + { + = ->getRecentViews(); + + // Don't include videos older than 10 days + if (->created_at->diffInDays(now()) > 10) { + return 0; + } + + // Don't include videos with no recent views + if ( < 5) { + return 0; + } + + // Calculate view velocity (views per hour in last 48 hours) + = / ; + + // Recency bonus: newer videos get a boost + = ->created_at->diffInHours(now()); + = max(0, 1 - ( / 240)); // Decreases over 10 days + + // Like count bonus + = ->like_count * 0.1; + + // Calculate final score + // Weight: 70% recent views, 15% velocity, 10% recency, 5% likes + = ( * 0.70) + + ( * 100 * 0.15) + + ( * 50 * 0.10) + + ( * 0.05); + + return round(, 2); + } + + // Scope for trending videos + public function scopeTrending(, = 48, = 50) + { + return ->public() + ->where('status', 'ready') + ->where('created_at', '>=', now()->subDays(10)) + ->orderByDesc(\DB::raw('( + (SELECT COUNT(*) FROM video_views vv WHERE vv.video_id = videos.id AND vv.watched_at >= DATE_SUB(NOW(), INTERVAL ' . . ' HOUR)) * 0.70 + + (SELECT COUNT(*) FROM video_views vv WHERE vv.video_id = videos.id AND vv.watched_at >= DATE_SUB(NOW(), INTERVAL ' . . ' HOUR)) / ' . . ' * 100 * 0.15 + + GREATEST(0, 1 - TIMESTAMPDIFF(HOUR, videos.created_at, NOW()) / 240) * 50 * 0.10 + + (SELECT COUNT(*) FROM video_likes vl WHERE vl.video_id = videos.id) * 0.1 * 0.05 + )')) + ->limit(); + } diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 8d81281..3b47bc9 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -415,7 +415,7 @@
-
+