Files
takeone-youtube-clone/app/Http/Controllers/SearchController.php
T
ghassanandClaude Opus 4.7 45b5f586ba feat(search): /search?q= across videos, channels and playlists
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]>
2026-10-21 10:09:40 +03:00

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' => [],
]);
}
}