![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/soundstudiopro.com/public_html/ |
<?php
// Error handling - ensure JSON is always returned
error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
// Set JSON header early
header('Content-Type: application/json');
// Function to return error JSON
function returnError($message, $code = 500) {
http_response_code($code);
echo json_encode([
'error' => $message,
'tracks' => [],
'artists' => [],
'genres' => [],
'events' => [],
'crates' => [],
'keywords' => []
]);
exit;
}
try {
session_start();
require_once 'config/database.php';
} catch (Exception $e) {
error_log('Global search API error loading dependencies: ' . $e->getMessage());
returnError('Failed to initialize search service', 500);
}
$query = trim($_GET['q'] ?? '');
$limit = min((int)($_GET['limit'] ?? 10), 20); // Max 20 results per category
if (empty($query) || strlen($query) < 2) {
echo json_encode([
'tracks' => [],
'artists' => [],
'genres' => [],
'events' => [],
'crates' => [],
'keywords' => []
]);
exit;
}
try {
$pdo = getDBConnection();
if (!$pdo) {
throw new Exception('Database connection failed');
}
} catch (Exception $e) {
error_log('Global search API database error: ' . $e->getMessage());
returnError('Database connection failed', 500);
}
// Create search parameter with wildcards for partial matching
// This will match "psy" in "Psytrance" or "deep" in "Deep House"
$search_param = '%' . strtolower($query) . '%';
// Debug logging (remove in production if needed)
error_log("Global search query: " . $query);
error_log("Global search param: " . $search_param);
$results = [
'tracks' => [],
'artists' => [],
'genres' => [],
'events' => [],
'crates' => [],
'keywords' => []
];
// Search tracks (title, artist, tags, metadata)
$tracks_sql = "
SELECT DISTINCT
mt.id,
mt.title,
mt.image_url,
mt.task_id,
mt.genre,
mt.tags,
mt.metadata,
u.name as artist_name,
u.id as artist_id,
u.profile_image,
COALESCE(play_stats.play_count, 0) as play_count
FROM music_tracks mt
INNER JOIN users u ON mt.user_id = u.id
LEFT JOIN (SELECT track_id, COUNT(*) as play_count FROM track_plays GROUP BY track_id) play_stats ON mt.id = play_stats.track_id
WHERE mt.status = 'complete'
AND (
(mt.audio_url IS NOT NULL AND mt.audio_url != '')
OR COALESCE(mt.variations_count, 0) > 0
)
AND mt.user_id IS NOT NULL
AND u.id IS NOT NULL
AND (mt.is_public = 1 OR mt.is_public IS NULL)
AND (
LOWER(mt.title) LIKE ?
OR LOWER(u.name) LIKE ?
OR LOWER(mt.tags) LIKE ?
OR LOWER(mt.genre) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.tags') AS CHAR)) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.tags'))) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.genre') AS CHAR)) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.genre'))) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.style') AS CHAR)) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.style'))) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.mood') AS CHAR)) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.mood'))) LIKE ?
)
ORDER BY play_stats.play_count DESC, mt.created_at DESC
LIMIT ?
";
try {
$stmt = $pdo->prepare($tracks_sql);
$stmt->execute([
$search_param, $search_param, $search_param, $search_param,
$search_param, $search_param, $search_param, $search_param,
$search_param, $search_param, $search_param, $search_param,
$limit
]);
$tracks = $stmt->fetchAll(PDO::FETCH_ASSOC);
error_log("Global search found " . count($tracks) . " tracks");
} catch (PDOException $e) {
error_log('Global search tracks query error: ' . $e->getMessage());
error_log('SQL: ' . $tracks_sql);
$tracks = [];
}
// Extract image URLs from metadata if image_url is empty
foreach ($tracks as &$track) {
$imageUrl = $track['image_url'] ?? null;
// If image_url is empty, try to get from metadata
if (empty($imageUrl) || $imageUrl === 'null' || $imageUrl === 'NULL') {
if (!empty($track['metadata'])) {
$metadata = is_string($track['metadata']) ? json_decode($track['metadata'], true) : $track['metadata'];
if (is_array($metadata)) {
// Check multiple possible locations in metadata
if (!empty($metadata['image_url'])) {
$imageUrl = $metadata['image_url'];
} elseif (!empty($metadata['cover_url'])) {
$imageUrl = $metadata['cover_url'];
} elseif (!empty($metadata['raw_callback']['image_url'])) {
$imageUrl = $metadata['raw_callback']['image_url'];
} elseif (!empty($metadata['raw_callback']['cover_url'])) {
$imageUrl = $metadata['raw_callback']['cover_url'];
} elseif (!empty($metadata['raw_callback']['data']['image_url'])) {
$imageUrl = $metadata['raw_callback']['data']['image_url'];
} elseif (!empty($metadata['raw_callback']['data']['cover_url'])) {
$imageUrl = $metadata['raw_callback']['data']['cover_url'];
}
}
}
// Normalize the image URL if found
if (!empty($imageUrl) && $imageUrl !== 'null' && $imageUrl !== 'NULL') {
// Only use local paths (no external URLs)
if (!str_starts_with($imageUrl, 'http://') && !str_starts_with($imageUrl, 'https://')) {
if (!str_starts_with($imageUrl, '/')) {
$imageUrl = '/' . ltrim($imageUrl, '/');
}
$track['image_url'] = $imageUrl;
} else {
$track['image_url'] = null;
}
} else {
$track['image_url'] = null;
}
} else {
// Normalize existing image_url
if (!str_starts_with($imageUrl, 'http://') && !str_starts_with($imageUrl, 'https://')) {
if (!str_starts_with($imageUrl, '/')) {
$imageUrl = '/' . ltrim($imageUrl, '/');
}
$track['image_url'] = $imageUrl;
} else {
$track['image_url'] = null;
}
}
// Fallback: Try to find image file by task_id pattern if image_url is still empty
if (empty($track['image_url']) || $track['image_url'] === 'null' || $track['image_url'] === 'NULL') {
if (!empty($track['task_id'])) {
$uploadsDir = $_SERVER['DOCUMENT_ROOT'] . '/uploads/track_covers/';
if (is_dir($uploadsDir)) {
$pattern = $uploadsDir . "track_{$track['task_id']}_*";
$files = glob($pattern);
if (!empty($files)) {
$mostRecent = end($files);
$track['image_url'] = '/uploads/track_covers/' . basename($mostRecent);
}
}
}
}
}
unset($track);
$results['tracks'] = $tracks;
// Search artists (all users, regardless of public track count)
$artists_sql = "
SELECT DISTINCT
u.id,
u.name,
COALESCE(up.profile_image, u.profile_image) as profile_image,
COALESCE(up.profile_position, 'center center') as profile_position,
up.bio,
up.location,
up.genres,
COALESCE((SELECT COUNT(*) FROM music_tracks WHERE user_id = u.id AND status = 'complete' AND (is_public = 1 OR is_public IS NULL)), 0) as track_count,
COALESCE((SELECT COUNT(*) FROM user_follows WHERE following_id = u.id), 0) as followers_count
FROM users u
LEFT JOIN user_profiles up ON u.id = up.user_id
WHERE (
u.name LIKE ?
OR up.bio LIKE ?
OR up.location LIKE ?
OR up.music_style LIKE ?
OR up.genres LIKE ?
)
ORDER BY followers_count DESC, track_count DESC, u.name ASC
LIMIT ?
";
try {
$stmt = $pdo->prepare($artists_sql);
$stmt->execute([
$search_param, $search_param, $search_param, $search_param,
$search_param, $limit
]);
$results['artists'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
error_log('Global search artists query error: ' . $e->getMessage());
$results['artists'] = [];
}
// Search events (title, description, location, venue)
$events_sql = "
SELECT
e.id,
e.title,
e.description,
e.event_type,
e.location,
e.venue_name,
e.start_date,
e.cover_image,
e.status,
u.name as creator_name,
u.id as creator_id
FROM events e
INNER JOIN users u ON e.creator_id = u.id
WHERE e.status = 'published'
AND (
e.title LIKE ?
OR e.description LIKE ?
OR e.location LIKE ?
OR e.venue_name LIKE ?
)
ORDER BY e.start_date ASC
LIMIT ?
";
try {
$stmt = $pdo->prepare($events_sql);
$stmt->execute([$search_param, $search_param, $search_param, $search_param, $limit]);
$results['events'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
error_log('Global search events query error: ' . $e->getMessage());
$results['events'] = [];
}
// Search crates (public playlists)
// Note: is_public = 1 OR is_public IS NULL to include crates without explicit public flag
$crates_sql = "
SELECT
ap.id,
ap.name,
ap.description,
ap.created_at,
COUNT(DISTINCT pt.track_id) as track_count,
COALESCE(SUM(mt.duration), 0) as total_duration,
u.name as artist_name,
u.id as artist_id,
COALESCE(up.profile_image, u.profile_image) as artist_image
FROM artist_playlists ap
INNER JOIN users u ON ap.user_id = u.id
LEFT JOIN user_profiles up ON u.id = up.user_id
LEFT JOIN playlist_tracks pt ON ap.id = pt.playlist_id
LEFT JOIN music_tracks mt ON pt.track_id = mt.id AND mt.status = 'complete'
WHERE (ap.is_public = 1 OR ap.is_public IS NULL)
AND (
LOWER(ap.name) LIKE ?
OR LOWER(ap.description) LIKE ?
OR LOWER(u.name) LIKE ?
)
GROUP BY ap.id, ap.name, ap.description, ap.created_at, u.name, u.id, up.profile_image, u.profile_image
HAVING track_count > 0
ORDER BY track_count DESC, ap.updated_at DESC
LIMIT ?
";
try {
$stmt = $pdo->prepare($crates_sql);
$stmt->execute([$search_param, $search_param, $search_param, $limit]);
$crates = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Format duration for display
foreach ($crates as &$crate) {
$totalSeconds = intval($crate['total_duration']);
$hours = floor($totalSeconds / 3600);
$minutes = floor(($totalSeconds % 3600) / 60);
$crate['duration_formatted'] = $hours > 0
? sprintf('%dh %dm', $hours, $minutes)
: sprintf('%dm', $minutes);
}
unset($crate);
$results['crates'] = $crates;
} catch (PDOException $e) {
error_log('Global search crates query error: ' . $e->getMessage());
$results['crates'] = [];
}
// Search genres specifically (from tracks and artist profiles)
// Use case-insensitive search and match partial words
// Also check metadata.style as some tracks store genre there
$genres_sql = "
SELECT DISTINCT
COALESCE(
JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.genre')),
JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.style')),
mt.genre
) as genre,
COUNT(DISTINCT mt.id) as track_count
FROM music_tracks mt
WHERE mt.status = 'complete'
AND (mt.is_public = 1 OR mt.is_public IS NULL)
AND (
LOWER(mt.genre) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.genre'))) LIKE ?
OR LOWER(JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.style'))) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.genre') AS CHAR)) LIKE ?
OR LOWER(CAST(JSON_EXTRACT(mt.metadata, '$.style') AS CHAR)) LIKE ?
)
AND COALESCE(
JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.genre')),
JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.style')),
mt.genre
) IS NOT NULL
AND COALESCE(
JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.genre')),
JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.style')),
mt.genre
) != ''
GROUP BY genre
HAVING genre IS NOT NULL AND genre != ''
ORDER BY track_count DESC, genre ASC
LIMIT ?
";
try {
$stmt = $pdo->prepare($genres_sql);
$stmt->execute([$search_param, $search_param, $search_param, $search_param, $search_param, $limit]);
$genres = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
error_log('Global search genres query error: ' . $e->getMessage());
$genres = [];
}
// Also search genres from artist profiles
$artist_genres_sql = "
SELECT DISTINCT
JSON_UNQUOTE(JSON_EXTRACT(up.genres, CONCAT('$[', idx.idx, ']'))) as genre
FROM user_profiles up
CROSS JOIN (
SELECT 0 as idx UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9
) idx
WHERE up.genres IS NOT NULL
AND LOWER(JSON_UNQUOTE(JSON_EXTRACT(up.genres, CONCAT('$[', idx.idx, ']')))) LIKE ?
AND JSON_UNQUOTE(JSON_EXTRACT(up.genres, CONCAT('$[', idx.idx, ']'))) IS NOT NULL
AND JSON_UNQUOTE(JSON_EXTRACT(up.genres, CONCAT('$[', idx.idx, ']'))) != ''
LIMIT ?
";
try {
$stmt = $pdo->prepare($artist_genres_sql);
$stmt->execute([$search_param, $limit]);
$artist_genres = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Merge artist genres with track genres
foreach ($artist_genres as $ag) {
if (!empty($ag['genre'])) {
$found = false;
foreach ($genres as &$g) {
if (strtolower(trim($g['genre'])) === strtolower(trim($ag['genre']))) {
$found = true;
break;
}
}
if (!$found) {
$genres[] = ['genre' => $ag['genre'], 'track_count' => 0];
}
}
}
} catch (Exception $e) {
// If JSON extraction fails, skip artist genres
}
// Filter and deduplicate genres
$unique_genres = [];
foreach ($genres as $g) {
if (!empty($g['genre']) && strlen($g['genre']) > 1) {
$key = strtolower(trim($g['genre']));
if (!isset($unique_genres[$key])) {
$unique_genres[$key] = [
'genre' => trim($g['genre']),
'track_count' => $g['track_count'] ?? 0
];
} else {
// Merge track counts if duplicate found
$unique_genres[$key]['track_count'] += ($g['track_count'] ?? 0);
}
}
}
// Sort by track count descending, then alphabetically
usort($unique_genres, function($a, $b) {
if ($a['track_count'] != $b['track_count']) {
return $b['track_count'] - $a['track_count'];
}
return strcasecmp($a['genre'], $b['genre']);
});
$results['genres'] = array_slice($unique_genres, 0, $limit);
// Extract unique keywords from tracks (style, mood, tags - excluding genres which are now separate)
$keywords_sql = "
SELECT DISTINCT
JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.style')) as keyword,
'style' as type
FROM music_tracks mt
WHERE mt.status = 'complete'
AND JSON_EXTRACT(mt.metadata, '$.style') IS NOT NULL
AND JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.style')) LIKE ?
AND JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.style')) != ''
UNION
SELECT DISTINCT
JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.mood')) as keyword,
'mood' as type
FROM music_tracks mt
WHERE mt.status = 'complete'
AND JSON_EXTRACT(mt.metadata, '$.mood') IS NOT NULL
AND JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.mood')) LIKE ?
AND JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.mood')) != ''
AND JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.mood')) != 'neutral'
UNION
SELECT DISTINCT
JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.tags')) as keyword,
'tag' as type
FROM music_tracks mt
WHERE mt.status = 'complete'
AND JSON_EXTRACT(mt.metadata, '$.tags') IS NOT NULL
AND JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.tags')) LIKE ?
AND JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.tags')) != ''
LIMIT ?
";
try {
$stmt = $pdo->prepare($keywords_sql);
$stmt->execute([$search_param, $search_param, $search_param, $limit]);
$keywords = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
error_log('Global search keywords query error: ' . $e->getMessage());
$keywords = [];
}
// Filter and deduplicate keywords - split comma-separated values
$unique_keywords = [];
foreach ($keywords as $kw) {
if (!empty($kw['keyword'])) {
$keywordValue = $kw['keyword'];
// Handle JSON arrays (tags might be stored as JSON array)
if (is_string($keywordValue) && (str_starts_with($keywordValue, '[') || str_starts_with($keywordValue, '"'))) {
$decoded = json_decode($keywordValue, true);
if (is_array($decoded)) {
// If it's an array, extract individual tags
foreach ($decoded as $tag) {
if (is_string($tag) && strlen(trim($tag)) > 1) {
$tag = trim($tag);
// Split by comma if it's a comma-separated string
$tags = explode(',', $tag);
foreach ($tags as $singleTag) {
$singleTag = trim($singleTag);
if (strlen($singleTag) > 1 && strlen($singleTag) < 50) {
$key = strtolower($singleTag);
if (!isset($unique_keywords[$key])) {
$unique_keywords[$key] = [
'keyword' => $singleTag,
'type' => $kw['type'] ?? 'tag'
];
}
}
}
}
}
continue;
} elseif (is_string($decoded)) {
$keywordValue = $decoded;
}
}
// Handle single string values - split by comma if comma-separated
if (is_string($keywordValue)) {
$keywordValue = trim($keywordValue);
// Split by comma to handle comma-separated keywords
$keywordParts = explode(',', $keywordValue);
foreach ($keywordParts as $part) {
$part = trim($part);
// Skip if too short, too long, or contains special characters that suggest it's code
if (strlen($part) > 1 && strlen($part) < 50 &&
!str_starts_with($part, '{') && !str_starts_with($part, '[') &&
!str_contains($part, '=>') && !str_contains($part, ':')) {
$key = strtolower($part);
if (!isset($unique_keywords[$key])) {
$unique_keywords[$key] = [
'keyword' => $part,
'type' => $kw['type'] ?? 'tag'
];
}
}
}
}
}
}
$results['keywords'] = array_values($unique_keywords);
// Ensure all required keys exist
$final_results = [
'tracks' => $results['tracks'] ?? [],
'artists' => $results['artists'] ?? [],
'genres' => $results['genres'] ?? [],
'events' => $results['events'] ?? [],
'crates' => $results['crates'] ?? [],
'keywords' => $results['keywords'] ?? []
];
// Debug: Log result counts
error_log("Global search results - Tracks: " . count($final_results['tracks']) .
", Artists: " . count($final_results['artists']) .
", Genres: " . count($final_results['genres']) .
", Events: " . count($final_results['events']) .
", Crates: " . count($final_results['crates']) .
", Keywords: " . count($final_results['keywords']));
// Output JSON with error handling
try {
echo json_encode($final_results, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (Exception $e) {
error_log('Global search JSON encoding error: ' . $e->getMessage());
returnError('Failed to format search results', 500);
}