Add a dedicated SearchController that matches the query against video title / description / tags / primary language AND any secondary audio track language (so "arabic" surfaces videos that carry an Arabic track even if the primary language is English), plus channel name / username / bio, plus playlist name / description. Public visibility only. Results reuse the videos/index view so every card gets the same component the browse feeds use. A new Channels section renders above the Videos grid when channel hits exist. /videos/search is kept as a 301 redirect to /search so bookmarks and external links stay valid; both the desktop header and mobile overlay search forms now point at the canonical route, both carry a visually hidden <label>, both get role="search" and both preserve the current query in their input. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
97 lines
3.7 KiB
PHP
97 lines
3.7 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\Playlist;
|
|
use App\Models\User;
|
|
use App\Models\Video;
|
|
use App\Models\VideoAudioTrack;
|
|
use Illuminate\Http\Request;
|
|
|
|
/**
|
|
* Site-wide search across videos, channels and playlists.
|
|
*
|
|
* Lives on /search?q=… so the URL reads like what you'd expect from a modern
|
|
* site. The old /videos/search endpoint still works but redirects here, so
|
|
* nothing bookmarked earlier breaks.
|
|
*
|
|
* Match surface:
|
|
* - Video title, description, tag list, primary language and the language
|
|
* of any secondary audio track attached to it.
|
|
* - User name, username (handle), and short bio.
|
|
* - Playlist name and description.
|
|
*
|
|
* We render the existing `videos.index` view so every search result lands in
|
|
* the same card component the browse feeds use — one place to style, one place
|
|
* to fix bugs.
|
|
*/
|
|
class SearchController extends Controller
|
|
{
|
|
public function index(Request $request)
|
|
{
|
|
$query = trim((string) $request->get('q', ''));
|
|
|
|
if ($query === '') {
|
|
// No query → send the user back to the browse feed instead of an
|
|
// empty "no results" page.
|
|
return redirect()->route('videos.index');
|
|
}
|
|
|
|
$like = '%' . $query . '%';
|
|
$qLower = mb_strtolower($query);
|
|
|
|
// ── Videos ────────────────────────────────────────────────
|
|
// Match on text columns AND on any audio-track language that aligns
|
|
// with the query (e.g. searching "arabic" surfaces videos that carry
|
|
// an Arabic track even if the primary language is English).
|
|
$trackVideoIds = VideoAudioTrack::whereRaw('LOWER(language) = ?', [$qLower])
|
|
->pluck('video_id');
|
|
|
|
$videoHits = Video::public()
|
|
->where(function ($q) use ($like, $qLower, $trackVideoIds) {
|
|
$q->where('title', 'like', $like)
|
|
->orWhere('description','like', $like)
|
|
->orWhere('tags', 'like', $like)
|
|
->orWhereRaw('LOWER(language) = ?', [$qLower])
|
|
->orWhereIn('id', $trackVideoIds);
|
|
})
|
|
->latest()
|
|
->limit(60)
|
|
->get();
|
|
|
|
$videos = $videoHits->where('is_shorts', false)->values();
|
|
$shorts = $videoHits->where('is_shorts', true)->values();
|
|
|
|
// ── Channels ──────────────────────────────────────────────
|
|
$channels = User::where(function ($q) use ($like) {
|
|
$q->where('name', 'like', $like)
|
|
->orWhere('username','like', $like)
|
|
->orWhere('bio', 'like', $like);
|
|
})
|
|
->orderByDesc('id')
|
|
->limit(12)
|
|
->get();
|
|
|
|
// ── Playlists ─────────────────────────────────────────────
|
|
$playlists = Playlist::where('visibility', 'public')
|
|
->where(function ($q) use ($like) {
|
|
$q->where('name', 'like', $like)
|
|
->orWhere('description','like', $like);
|
|
})
|
|
->withCount('videos')
|
|
->with(['user', 'videos' => fn ($q) => $q->orderBy('playlist_videos.position')->limit(1)])
|
|
->latest()
|
|
->limit(12)
|
|
->get();
|
|
|
|
return view('videos.index', [
|
|
'videos' => $videos,
|
|
'shorts' => $shorts,
|
|
'playlists' => $playlists,
|
|
'channels' => $channels,
|
|
'query' => $query,
|
|
'langChips' => [],
|
|
]);
|
|
}
|
|
}
|