diff --git a/app/Http/Controllers/VideoController.php b/app/Http/Controllers/VideoController.php index 2f63a02..9498dbd 100644 --- a/app/Http/Controllers/VideoController.php +++ b/app/Http/Controllers/VideoController.php @@ -858,11 +858,59 @@ class VideoController extends Controller } } - $recommendedVideos = Video::public() - ->where('id', '!=', $video->id) - ->latest() - ->limit(20) - ->get(); + // Up Next recommendations: a weighted blend instead of the plain + // chronological "newest 20" the sidebar used to show. + // + // +4 same type (music / match / generic) + // +3 same primary language + // +2 overlapping tag (any match in the comma-separated tags list) + // +1 same channel (keeps a creator's recent uploads surfaced, but + // does not dominate — cross-channel results compete on type/ + // language/tags scoring) + // + // Candidates: the newest ~200 public ready videos excluding the one + // being watched. 200 is big enough for the blend to find real + // similarities, small enough to score in-process without a query + // per candidate. + $recommendedVideos = (function () use ($video) { + $candidates = Video::public() + ->where('status', 'ready') + ->where('id', '!=', $video->id) + ->with('user') + ->latest('id') + ->limit(200) + ->get(); + + $ownTags = collect(explode(',', (string) ($video->tags ?? ''))) + ->map(fn ($t) => mb_strtolower(trim($t))) + ->filter() + ->unique(); + + $scored = $candidates->map(function (Video $cand) use ($video, $ownTags) { + $score = 0; + if ($cand->type === $video->type) $score += 4; + if ($video->language && $cand->language === $video->language) $score += 3; + if ($ownTags->isNotEmpty()) { + $candTags = collect(explode(',', (string) ($cand->tags ?? ''))) + ->map(fn ($t) => mb_strtolower(trim($t))) + ->filter(); + if ($ownTags->intersect($candTags)->isNotEmpty()) $score += 2; + } + if ($cand->user_id === $video->user_id) $score += 1; + + $cand->_rec_score = $score; + return $cand; + }); + + // Sort by score desc, then newest. Keep up to 20. + return $scored + ->sortBy([ + ['_rec_score', 'desc'], + ['created_at', 'desc'], + ]) + ->take(20) + ->values(); + })(); $view = match ($video->type) { 'match' => 'videos.types.match', diff --git a/resources/views/videos/show.blade.php b/resources/views/videos/show.blade.php index 395e405..10f65b7 100644 --- a/resources/views/videos/show.blade.php +++ b/resources/views/videos/show.blade.php @@ -836,30 +836,38 @@ @if ($recommendedVideos && $recommendedVideos->count() > 0) + @endforeach @else diff --git a/resources/views/videos/types/generic.blade.php b/resources/views/videos/types/generic.blade.php index 643095b..36e4206 100644 --- a/resources/views/videos/types/generic.blade.php +++ b/resources/views/videos/types/generic.blade.php @@ -307,7 +307,10 @@ border-radius: 8px; padding: 4px; transition: background .15s; + color: inherit; + text-decoration: none; } + .sidebar-video-card:hover { background: rgba(255,255,255,.05); } .sidebar-video-card.current-video { background: rgba(239,68,68,.12); diff --git a/resources/views/videos/types/music.blade.php b/resources/views/videos/types/music.blade.php index 524a51a..120380f 100644 --- a/resources/views/videos/types/music.blade.php +++ b/resources/views/videos/types/music.blade.php @@ -307,7 +307,12 @@ border-radius: 8px; padding: 4px; transition: background .15s; + /* Card is now an for real right-click / open-in-new-tab + / middle-click support. Strip the default link styling. */ + color: inherit; + text-decoration: none; } + .sidebar-video-card:hover { background: rgba(255,255,255,.05); } .sidebar-video-card.current-video { background: rgba(239,68,68,.12);