![]() 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
require_once 'config/database.php';
require_once 'includes/translations.php';
require_once 'utils/audio_token.php';
require_once 'utils/copyright_protection.php';
session_start();
$user_id = $_SESSION['user_id'] ?? null;
// SECURITY: Validate and sanitize track_id parameter
$track_id_raw = $_GET['id'] ?? null;
// Check if this is an AJAX request
$isAjaxRequest = isset($_GET['ajax']) && $_GET['ajax'] == '1';
if (!$track_id_raw) {
if ($isAjaxRequest) {
// For AJAX requests, return error content instead of redirecting
echo json_encode(['success' => false, 'error' => 'No track ID provided']);
exit;
} else {
header('Location: /community_fixed.php');
exit;
}
}
// SECURITY: Validate that track_id is a positive integer
// This prevents SQL injection attempts and ensures type safety
if (!is_numeric($track_id_raw) || (int)$track_id_raw <= 0) {
error_log("SECURITY: Invalid track_id attempt: " . htmlspecialchars($track_id_raw, ENT_QUOTES, 'UTF-8'));
if ($isAjaxRequest) {
echo json_encode(['success' => false, 'error' => 'Invalid track ID']);
exit;
} else {
header('Location: /community_fixed.php');
exit;
}
}
// Cast to integer for safety (prepared statements will handle this, but this adds extra validation)
$track_id = (int)$track_id_raw;
try {
$pdo = getDBConnection();
// Check if user is admin
$is_admin = isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true;
// Check if track_votes table exists
$votes_table_exists = false;
try {
$check_stmt = $pdo->query("SHOW TABLES LIKE 'track_votes'");
$votes_table_exists = $check_stmt->rowCount() > 0;
} catch (Exception $e) {
$votes_table_exists = false;
}
// Get the specific track with all details including ratings
// Allow owners and admins to view processing/failed tracks, but public can only see complete tracks
if ($votes_table_exists) {
$track_query = "
SELECT
mt.*,
u.name as artist_name,
u.plan as artist_plan,
COALESCE(tp.play_count, 0) as play_count,
COALESCE(tl.like_count, 0) as like_count,
COALESCE(tr_avg.average_rating, 0) as average_rating,
COALESCE(tr_count.rating_count, 0) as rating_count,
COALESCE(tv.vote_count, 0) as vote_count,
CASE WHEN tl_user.track_id IS NOT NULL THEN 1 ELSE 0 END as user_liked,
tv_user.vote_type as user_vote
FROM music_tracks mt
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) tp ON mt.id = tp.track_id
LEFT JOIN (SELECT track_id, COUNT(*) as like_count FROM track_likes GROUP BY track_id) tl ON mt.id = tl.track_id
LEFT JOIN (SELECT track_id, AVG(rating) as average_rating FROM track_ratings GROUP BY track_id) tr_avg ON mt.id = tr_avg.track_id
LEFT JOIN (SELECT track_id, COUNT(*) as rating_count FROM track_ratings GROUP BY track_id) tr_count ON mt.id = tr_count.track_id
LEFT JOIN (
SELECT
track_id,
SUM(CASE WHEN vote_type = 'up' THEN 1 ELSE -1 END) as vote_count
FROM track_votes
GROUP BY track_id
) tv ON mt.id = tv.track_id
LEFT JOIN track_likes tl_user ON mt.id = tl_user.track_id AND tl_user.user_id = ?
LEFT JOIN track_votes tv_user ON mt.id = tv_user.track_id AND tv_user.user_id = ?
WHERE mt.id = ?
AND (
mt.status = 'complete'
OR (mt.user_id = ? AND mt.status IN ('processing', 'failed'))
OR (? = 1)
)
";
$stmt = $pdo->prepare($track_query);
$stmt->execute([$user_id, $user_id, $track_id, $user_id, $is_admin ? 1 : 0]);
} else {
$track_query = "
SELECT
mt.*,
u.name as artist_name,
u.plan as artist_plan,
COALESCE(tp.play_count, 0) as play_count,
COALESCE(tl.like_count, 0) as like_count,
COALESCE(tr_avg.average_rating, 0) as average_rating,
COALESCE(tr_count.rating_count, 0) as rating_count,
0 as vote_count,
CASE WHEN tl_user.track_id IS NOT NULL THEN 1 ELSE 0 END as user_liked,
NULL as user_vote
FROM music_tracks mt
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) tp ON mt.id = tp.track_id
LEFT JOIN (SELECT track_id, COUNT(*) as like_count FROM track_likes GROUP BY track_id) tl ON mt.id = tl.track_id
LEFT JOIN (SELECT track_id, AVG(rating) as average_rating FROM track_ratings GROUP BY track_id) tr_avg ON mt.id = tr_avg.track_id
LEFT JOIN (SELECT track_id, COUNT(*) as rating_count FROM track_ratings GROUP BY track_id) tr_count ON mt.id = tr_count.track_id
LEFT JOIN track_likes tl_user ON mt.id = tl_user.track_id AND tl_user.user_id = ?
WHERE mt.id = ?
AND (
mt.status = 'complete'
OR (mt.user_id = ? AND mt.status IN ('processing', 'failed'))
OR (? = 1)
)
";
$stmt = $pdo->prepare($track_query);
$stmt->execute([$user_id, $track_id, $user_id, $is_admin ? 1 : 0]);
}
$track = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$track) {
if ($isAjaxRequest) {
// For AJAX requests, return error content instead of redirecting
echo json_encode(['success' => false, 'error' => 'Track not found or not accessible']);
exit;
} else {
header('Location: /community_fixed.php');
exit;
}
}
// SECURITY: Check if track is private and user has access
// Only the track owner can view private tracks, OR users with a valid share token
// IMPORTANT: Allow only known legitimate bots/crawlers (like Facebook scraper) to access for OG tags
// SECURITY: Only allow specific known bots - prevent user agent spoofing to bypass access control
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
$knownBots = [
'facebookexternalhit', // Facebook
'Twitterbot', // Twitter/X
'LinkedInBot', // LinkedIn
'WhatsApp', // WhatsApp
'Googlebot', // Google
'Bingbot', // Bing
'Slackbot', // Slack
'Discordbot', // Discord
'Applebot', // Apple
'YandexBot', // Yandex
'Baiduspider', // Baidu
];
$isBot = false;
foreach ($knownBots as $bot) {
if (stripos($userAgent, $bot) !== false) {
$isBot = true;
break;
}
}
// Check for share token
$share_token = $_GET['share'] ?? '';
$hasValidShareToken = false;
$share_token_expires = null;
if (!empty($share_token)) {
require_once __DIR__ . '/utils/share_token.php';
$hasValidShareToken = isValidShareToken($track_id, $share_token);
// Get expiration time for display in description
if ($hasValidShareToken) {
$pdo = getDBConnection();
if ($pdo) {
$stmt = $pdo->prepare("SELECT share_token_expires FROM music_tracks WHERE id = ? AND share_token = ?");
$stmt->execute([$track_id, $share_token]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if ($result && !empty($result['share_token_expires'])) {
$share_token_expires = (int)$result['share_token_expires'];
}
}
}
}
if ($track['is_public'] != 1 && $track['is_public'] !== null) {
// Track is private (is_public = 0)
// IMPORTANT: Always allow bots to access private tracks for OG tags (they need the metadata for social sharing)
// This is critical for Facebook, Twitter, etc. to scrape the page and show previews
// Regular users need: ownership OR valid share token OR bot
$isOwner = ($user_id && $track['user_id'] == $user_id);
// Bots always get access for OG tag scraping (even without share token)
// This ensures social media platforms can always scrape the page metadata
if (!$isBot && !$isOwner && !$hasValidShareToken) {
// User is not the owner, not a bot, and doesn't have a valid share token - deny access
if ($isAjaxRequest) {
echo json_encode(['success' => false, 'error' => 'Access denied. This track is private.']);
exit;
} else {
header('Location: /community_fixed.php');
exit;
}
}
// Bot, owner, or has valid share token - allow access (continue)
}
// Track is public (is_public = 1 or null) - allow access for everyone (including bots for OG tags)
// AUTOMATIC LYRICS EXTRACTION: If track is complete but missing lyrics, try to extract from task_results
// Also check if existing lyrics are corrupted (no newlines when they should have structure)
$hasLyrics = !empty($track['lyrics']) && trim($track['lyrics']) !== '';
$lyricsCorrupted = false;
// Check if lyrics exist but are corrupted (no newlines, likely all on one line)
if ($hasLyrics && strlen($track['lyrics']) > 100) {
// If lyrics are long but have no newlines, they're likely corrupted
$lyricsCorrupted = (strpos($track['lyrics'], "\n") === false && strpos($track['lyrics'], "\r") === false);
if ($lyricsCorrupted) {
error_log("π Track $track_id has corrupted lyrics (no line breaks detected), attempting re-extraction...");
}
}
if ($track['status'] === 'complete' && (!$hasLyrics || $lyricsCorrupted) && !empty($track['task_id'])) {
if (!$hasLyrics) {
error_log("π Track $track_id missing lyrics (status: {$track['status']}, task_id: {$track['task_id']}), attempting extraction...");
}
$fallbackLyrics = extractLyricsFromTaskResults($track['task_id']);
if ($fallbackLyrics && !empty(trim($fallbackLyrics))) {
// Only update if the new lyrics are better (have newlines) or if we had no lyrics
$newLyricsHasStructure = (strpos($fallbackLyrics, "\n") !== false || strpos($fallbackLyrics, "\r") !== false);
if (!$hasLyrics || ($lyricsCorrupted && $newLyricsHasStructure)) {
// Update track with extracted lyrics
$stmt = $pdo->prepare("UPDATE music_tracks SET lyrics = ? WHERE id = ?");
if ($stmt->execute([$fallbackLyrics, $track_id])) {
// Update the track array so it's available for display
$track['lyrics'] = $fallbackLyrics;
error_log("β
Successfully auto-extracted and saved lyrics for track $track_id (length: " . strlen($fallbackLyrics) . " chars)");
} else {
$errorInfo = $stmt->errorInfo();
error_log("β Failed to save extracted lyrics to database for track $track_id: " . json_encode($errorInfo));
}
}
} else {
error_log("β οΈ Could not extract lyrics from task_results for track $track_id (task_id: " . $track['task_id'] . ")");
}
} elseif ($track['status'] === 'complete' && $hasLyrics && !$lyricsCorrupted) {
error_log("β
Track $track_id already has lyrics (length: " . strlen($track['lyrics']) . " chars)");
} elseif (empty($track['task_id'])) {
error_log("β οΈ Track $track_id has no task_id, cannot extract lyrics");
}
// Normalize lyrics for consistent display
if (!empty($track['lyrics'])) {
// Strip any existing HTML tags
$track['lyrics'] = strip_tags($track['lyrics']);
// Normalize line breaks (convert \r\n and \r to \n)
$track['lyrics'] = str_replace(["\r\n", "\r"], "\n", $track['lyrics']);
// Remove excessive blank lines (more than 2 consecutive newlines become 2 newlines)
$track['lyrics'] = preg_replace('/\n{3,}/', "\n\n", $track['lyrics']);
// Trim whitespace from start and end
$track['lyrics'] = trim($track['lyrics']);
}
// Calculate track rankings based on multiple metrics
$rankings = [];
if ($track['status'] === 'complete') {
// Get total tracks for context
$total_tracks_query = "SELECT COUNT(*) FROM music_tracks WHERE status = 'complete'";
$stmt_total = $pdo->query($total_tracks_query);
$total_tracks = $stmt_total->fetchColumn();
// Overall ranking (weighted score) - among all complete tracks
// Tiebreaker: When scores are equal, newer tracks (created_at) rank higher
if ($votes_table_exists) {
$overall_rank_query = "
SELECT
COUNT(*) + 1 as rank
FROM music_tracks mt2
LEFT JOIN (SELECT track_id, COUNT(*) as play_count FROM track_plays GROUP BY track_id) tp2 ON mt2.id = tp2.track_id
LEFT JOIN (SELECT track_id, COUNT(*) as like_count FROM track_likes GROUP BY track_id) tl2 ON mt2.id = tl2.track_id
LEFT JOIN (
SELECT
track_id,
SUM(CASE WHEN vote_type = 'up' THEN 1 ELSE -1 END) as vote_count
FROM track_votes
GROUP BY track_id
) tv2 ON mt2.id = tv2.track_id
LEFT JOIN (SELECT track_id, AVG(rating) as avg_rating, COUNT(*) as rating_count FROM track_ratings GROUP BY track_id) tr2 ON mt2.id = tr2.track_id
WHERE mt2.status = 'complete'
AND (
(COALESCE(tp2.play_count, 0) * 1.0 +
COALESCE(tl2.like_count, 0) * 2.0 +
COALESCE(tv2.vote_count, 0) * 3.0 +
COALESCE(tr2.avg_rating, 0) * COALESCE(tr2.rating_count, 0) * 5.0) >
(COALESCE(?, 0) * 1.0 +
COALESCE(?, 0) * 2.0 +
COALESCE(?, 0) * 3.0 +
COALESCE(?, 0) * COALESCE(?, 0) * 5.0)
OR (
(COALESCE(tp2.play_count, 0) * 1.0 +
COALESCE(tl2.like_count, 0) * 2.0 +
COALESCE(tv2.vote_count, 0) * 3.0 +
COALESCE(tr2.avg_rating, 0) * COALESCE(tr2.rating_count, 0) * 5.0) =
(COALESCE(?, 0) * 1.0 +
COALESCE(?, 0) * 2.0 +
COALESCE(?, 0) * 3.0 +
COALESCE(?, 0) * COALESCE(?, 0) * 5.0)
AND mt2.created_at > ?
)
)
";
$stmt_rank = $pdo->prepare($overall_rank_query);
$stmt_rank->execute([
$track['play_count'],
$track['like_count'],
$track['vote_count'] ?? 0,
$track['average_rating'] ?? 0,
$track['rating_count'] ?? 0,
$track['play_count'],
$track['like_count'],
$track['vote_count'] ?? 0,
$track['average_rating'] ?? 0,
$track['rating_count'] ?? 0,
$track['created_at']
]);
} else {
$overall_rank_query = "
SELECT
COUNT(*) + 1 as rank
FROM music_tracks mt2
LEFT JOIN (SELECT track_id, COUNT(*) as play_count FROM track_plays GROUP BY track_id) tp2 ON mt2.id = tp2.track_id
LEFT JOIN (SELECT track_id, COUNT(*) as like_count FROM track_likes GROUP BY track_id) tl2 ON mt2.id = tl2.track_id
LEFT JOIN (SELECT track_id, AVG(rating) as avg_rating, COUNT(*) as rating_count FROM track_ratings GROUP BY track_id) tr2 ON mt2.id = tr2.track_id
WHERE mt2.status = 'complete'
AND (
(COALESCE(tp2.play_count, 0) * 1.0 +
COALESCE(tl2.like_count, 0) * 2.0 +
COALESCE(tr2.avg_rating, 0) * COALESCE(tr2.rating_count, 0) * 5.0) >
(COALESCE(?, 0) * 1.0 +
COALESCE(?, 0) * 2.0 +
COALESCE(?, 0) * COALESCE(?, 0) * 5.0)
OR (
(COALESCE(tp2.play_count, 0) * 1.0 +
COALESCE(tl2.like_count, 0) * 2.0 +
COALESCE(tr2.avg_rating, 0) * COALESCE(tr2.rating_count, 0) * 5.0) =
(COALESCE(?, 0) * 1.0 +
COALESCE(?, 0) * 2.0 +
COALESCE(?, 0) * COALESCE(?, 0) * 5.0)
AND mt2.created_at > ?
)
)
";
$stmt_rank = $pdo->prepare($overall_rank_query);
$stmt_rank->execute([
$track['play_count'],
$track['like_count'],
$track['average_rating'] ?? 0,
$track['rating_count'] ?? 0,
$track['play_count'],
$track['like_count'],
$track['average_rating'] ?? 0,
$track['rating_count'] ?? 0,
$track['created_at']
]);
}
$rankings['overall'] = $stmt_rank->fetchColumn();
// Calculate total score for logging
$total_score = ($track['play_count'] * 1.0) +
($track['like_count'] * 2.0) +
(($track['vote_count'] ?? 0) * 3.0) +
(($track['average_rating'] ?? 0) * ($track['rating_count'] ?? 0) * 5.0);
// Audit log: Track ranking calculated
error_log("π RANKING CALC: Track ID {$track['id']} ('{$track['title']}') - Rank #{$rankings['overall']}, Score: " . round($total_score, 2) . " (Plays: {$track['play_count']}, Likes: {$track['like_count']}, Votes: " . ($track['vote_count'] ?? 0) . ", Rating: " . round($track['average_rating'] ?? 0, 1) . "x{$track['rating_count']})");
// Play count ranking - among all complete tracks
$play_rank_query = "
SELECT COUNT(*) + 1
FROM music_tracks mt2
LEFT JOIN (SELECT track_id, COUNT(*) as play_count FROM track_plays GROUP BY track_id) tp2 ON mt2.id = tp2.track_id
WHERE mt2.status = 'complete'
AND COALESCE(tp2.play_count, 0) > ?
";
$stmt_rank = $pdo->prepare($play_rank_query);
$stmt_rank->execute([$track['play_count']]);
$rankings['plays'] = $stmt_rank->fetchColumn();
// Like count ranking - among all complete tracks
$like_rank_query = "
SELECT COUNT(*) + 1
FROM music_tracks mt2
LEFT JOIN (SELECT track_id, COUNT(*) as like_count FROM track_likes GROUP BY track_id) tl2 ON mt2.id = tl2.track_id
WHERE mt2.status = 'complete'
AND COALESCE(tl2.like_count, 0) > ?
";
$stmt_rank = $pdo->prepare($like_rank_query);
$stmt_rank->execute([$track['like_count']]);
$rankings['likes'] = $stmt_rank->fetchColumn();
// Rating ranking (based on average rating with minimum rating count)
if (($track['rating_count'] ?? 0) >= 3) {
$rating_rank_query = "
SELECT COUNT(*) + 1
FROM music_tracks mt2
LEFT JOIN (SELECT track_id, AVG(rating) as avg_rating, COUNT(*) as rating_count FROM track_ratings GROUP BY track_id) tr2 ON mt2.id = tr2.track_id
WHERE mt2.status = 'complete'
AND COALESCE(tr2.rating_count, 0) >= 3
AND COALESCE(tr2.avg_rating, 0) > ?
";
$stmt_rank = $pdo->prepare($rating_rank_query);
$stmt_rank->execute([$track['average_rating'] ?? 0]);
$rankings['rating'] = $stmt_rank->fetchColumn();
}
$rankings['total_tracks'] = $total_tracks;
$rankings['overall_percentile'] = round((($total_tracks - $rankings['overall']) / max($total_tracks, 1)) * 100, 1);
// Calculate detailed score breakdown for display
$rankings['score_breakdown'] = [
'plays_score' => $track['play_count'] * 1.0,
'likes_score' => $track['like_count'] * 2.0,
'votes_score' => ($track['vote_count'] ?? 0) * 3.0,
'rating_score' => ($track['average_rating'] ?? 0) * ($track['rating_count'] ?? 0) * 5.0,
'total_score' => ($track['play_count'] * 1.0) + ($track['like_count'] * 2.0) + (($track['vote_count'] ?? 0) * 3.0) + (($track['average_rating'] ?? 0) * ($track['rating_count'] ?? 0) * 5.0)
];
}
// Parse metadata
$metadata = json_decode($track['metadata'] ?? '{}', true) ?: [];
$genre = $metadata['genre'] ?? null;
// DEBUG: Log what we're reading from metadata
if (defined('DEBUG_MODE') && DEBUG_MODE) {
error_log("π Track $track_id metadata: " . json_encode([
'bpm' => $metadata['bpm'] ?? 'null',
'key' => $metadata['key'] ?? 'null',
'numerical_key' => $metadata['numerical_key'] ?? 'null',
'analysis_source' => $metadata['analysis_source'] ?? 'null',
'analysis_date' => $metadata['analysis_date'] ?? 'null'
]));
}
// SINGLE SOURCE OF TRUTH: Read from main fields only
// All analysis results are stored in bpm/key/numerical_key/energy
$rawBpm = $metadata['bpm'] ?? null;
$key = $metadata['key'] ?? null;
$camelot = $metadata['numerical_key'] ?? null;
$energy = $metadata['energy'] ?? null;
// CRITICAL: Real-world music BPM is 60-140 max. Normalize to match save logic
$bpm = $rawBpm;
if ($bpm) {
// CRITICAL: Values > 140 NEVER happen in real music - halve immediately
if ($bpm > 140) {
$bpm = $bpm / 2;
// If still > 140 after halving, halve again (safety)
if ($bpm > 140) {
$bpm = $bpm / 2;
}
}
// 188 is a common error (94*2) - fix it specifically
elseif (abs($bpm - 188) < 2) {
$bpm = 94;
}
// Values < 50: Very low, likely wrong
elseif ($bpm < 50 && $bpm > 0) {
$doubled = $bpm * 2;
if ($doubled <= 140) {
$bpm = $doubled;
}
}
// Clamp to realistic range (60-140)
if ($bpm > 140) {
$bpm = 140;
} elseif ($bpm < 40) {
$bpm = 60;
}
}
// Check if we have analyzed (real) data or just defaults
$hasAnalyzedBPM = isset($metadata['bpm']) && isset($metadata['analysis_source']);
$hasAnalyzedKey = isset($metadata['key']) && isset($metadata['analysis_source']);
$hasAnalyzedEnergy = isset($metadata['analyzed_energy']) ||
(isset($metadata['energy']) && isset($metadata['analysis_source']));
// These can use defaults as they're less critical
$time_signature = $metadata['time_signature'] ?? '4/4';
$instruments = $metadata['instruments'] ?? [];
$mood = $metadata['mood'] ?? null;
// Audio analysis feature removed - BPM/Key displayed from database only
// CRITICAL: Get variations and check for selected variation
$trackVariations = [];
$selectedVariation = null;
$selectedVariationIndex = null;
// Check for selected variation in metadata
if (isset($metadata['selected_variation'])) {
$selectedVariationIndex = (int)$metadata['selected_variation'];
} elseif (isset($track['selected_variation'])) {
$selectedVariationIndex = (int)$track['selected_variation'];
}
// CRITICAL FIX: Always check for variations in database, regardless of variations_count field
// This ensures we find variations even if variations_count is incorrect (0 when there are actually variations)
try {
$var_stmt = $pdo->prepare("
SELECT
variation_index,
audio_url,
duration,
title,
tags,
image_url,
source_audio_url
FROM audio_variations
WHERE track_id = ?
ORDER BY variation_index ASC
");
$var_stmt->execute([$track['id']]);
$trackVariations = $var_stmt->fetchAll(PDO::FETCH_ASSOC);
// If we found variations but variations_count is wrong, update it
if (count($trackVariations) > 0 && $track['variations_count'] != count($trackVariations)) {
error_log("β οΈ Track {$track['id']}: Found " . count($trackVariations) . " variations but variations_count is {$track['variations_count']}. Auto-fixing...");
$update_stmt = $pdo->prepare("UPDATE music_tracks SET variations_count = ? WHERE id = ?");
$update_stmt->execute([count($trackVariations), $track['id']]);
$track['variations_count'] = count($trackVariations); // Update local variable
}
// Find the selected variation
if ($selectedVariationIndex !== null && !empty($trackVariations)) {
foreach ($trackVariations as $var) {
if (isset($var['variation_index']) && $var['variation_index'] == $selectedVariationIndex) {
$selectedVariation = $var;
break;
}
}
// Fallback: use array index if variation_index doesn't match
if (!$selectedVariation && isset($trackVariations[$selectedVariationIndex])) {
$selectedVariation = $trackVariations[$selectedVariationIndex];
}
}
} catch (Exception $e) {
error_log("Error getting variations for track {$track['id']}: " . $e->getMessage());
$trackVariations = [];
}
// Use selected variation's audio URL if available, otherwise use main track's audio URL
$playbackAudioUrl = $track['audio_url'];
$playbackDuration = $track['duration'];
if ($selectedVariation && !empty($selectedVariation['audio_url'])) {
$playbackAudioUrl = $selectedVariation['audio_url'];
if (!empty($selectedVariation['duration'])) {
$playbackDuration = $selectedVariation['duration'];
}
error_log("β
Using selected variation {$selectedVariationIndex} audio URL for track {$track['id']}");
}
// Parse enhanced metadata for JavaScript
$audio_quality = json_decode($track['audio_quality'] ?? '{}', true) ?: [];
$generation_parameters = json_decode($track['generation_parameters'] ?? '{}', true) ?: [];
$processing_info = json_decode($track['processing_info'] ?? '{}', true) ?: [];
$cost_info = json_decode($track['cost_info'] ?? '{}', true) ?: [];
// Check if this is a variation and get the main track title
// Variations share the same task_id, so find the main track (earliest created or lowest ID)
if (!empty($track['task_id'])) {
$main_track_query = "
SELECT id, title
FROM music_tracks
WHERE task_id = ? AND status = 'complete'
ORDER BY created_at ASC, id ASC
LIMIT 1
";
$main_track_stmt = $pdo->prepare($main_track_query);
$main_track_stmt->execute([$track['task_id']]);
$main_track = $main_track_stmt->fetch(PDO::FETCH_ASSOC);
// If main track exists and is different from current track, use main track's title
if ($main_track && $main_track['id'] != $track['id'] && !empty($main_track['title'])) {
$display_title = $main_track['title'];
} else {
$display_title = $track['title'];
}
} else {
$display_title = $track['title'];
}
} catch (Exception $e) {
error_log("Track page error: " . $e->getMessage());
if ($isAjaxRequest) {
// For AJAX requests, return error content instead of redirecting
echo json_encode(['success' => false, 'error' => 'Failed to load track']);
exit;
} else {
header('Location: /community_fixed.php');
exit;
}
}
// SEO-optimized page title: Include track name, artist, and brand for better search visibility
// Use translations for multi-language support
$track_title_clean = htmlspecialchars($display_title ?? $track['title']);
$artist_name_clean = htmlspecialchars($track['artist_name']);
$page_title = t('seo.track.title', [
'track' => $track_title_clean,
'artist' => $artist_name_clean
]);
// Enhanced description for better SEO - includes track name, artist, and platform
// Add BPM and key to description if available (helps with DJ searches)
$description_extras = [];
if (!empty($bpm) && $bpm > 0) {
$description_extras[] = round($bpm) . ' ' . t('seo.track.bpm');
}
if (!empty($camelot)) {
$description_extras[] = t('seo.track.camelot') . ' ' . $camelot;
} elseif (!empty($key)) {
$description_extras[] = t('seo.track.key') . ' ' . $key;
}
if (!empty($description_extras)) {
$metadata_string = implode(', ', $description_extras);
$page_description = t('seo.track.description_with_metadata', [
'track' => $track_title_clean,
'artist' => $artist_name_clean,
'metadata' => $metadata_string
]);
} else {
$page_description = t('seo.track.description', [
'track' => $track_title_clean,
'artist' => $artist_name_clean
]);
}
// Add expiration notice if share token is present and valid
if ($hasValidShareToken && $share_token_expires !== null) {
$time_remaining = $share_token_expires - time();
if ($time_remaining > 0) {
// Format expiration time in a user-friendly way
$hours = floor($time_remaining / 3600);
$days = floor($time_remaining / 86400);
$expiration_text = '';
if ($days >= 1) {
$expiration_text = $days == 1 ? '1 DAY' : $days . ' DAYS';
} elseif ($hours >= 1) {
$expiration_text = $hours == 1 ? '1 HOUR' : $hours . ' HOURS';
} else {
$minutes = floor($time_remaining / 60);
$expiration_text = $minutes == 1 ? '1 MINUTE' : $minutes . ' MINUTES';
}
// Add expiration notice to description for OG tags (shorter format)
$page_description .= ' | LINK VALID ' . $expiration_text;
}
}
// Build page URL - include share token if present for proper OG tag generation
$page_url = 'https://soundstudiopro.com/track.php?id=' . $track_id;
if (!empty($share_token)) {
$page_url .= '&share=' . urlencode($share_token);
}
// Normalize track image URL - same logic as library.php
$trackImageUrl = $track['image_url'] ?? null;
// If image_url exists and is not empty/null, use it (normalize local paths)
if (!empty($trackImageUrl) && $trackImageUrl !== 'null' && $trackImageUrl !== 'NULL') {
// Normalize local paths (not external URLs)
if (!preg_match('/^https?:\/\//', $trackImageUrl)) {
if (!str_starts_with($trackImageUrl, '/')) {
$trackImageUrl = '/' . ltrim($trackImageUrl, '/');
}
}
// If it's external, use it as-is (original image from database)
} else {
// Only if image_url is empty/null, try fallback sources
if (!empty($track['metadata'])) {
$metadata = is_string($track['metadata']) ? json_decode($track['metadata'], true) : $track['metadata'];
if (isset($metadata['image_url']) && !empty($metadata['image_url'])) {
$metaImageUrl = $metadata['image_url'];
if (strpos($metaImageUrl, 'http://') !== 0 && strpos($metaImageUrl, 'https://') !== 0) {
if (!str_starts_with($metaImageUrl, '/')) {
$metaImageUrl = '/' . ltrim($metaImageUrl, '/');
}
$trackImageUrl = $metaImageUrl;
}
} elseif (isset($metadata['cover_url']) && !empty($metadata['cover_url'])) {
$metaCoverUrl = $metadata['cover_url'];
if (strpos($metaCoverUrl, 'http://') !== 0 && strpos($metaCoverUrl, 'https://') !== 0) {
if (!str_starts_with($metaCoverUrl, '/')) {
$metaCoverUrl = '/' . ltrim($metaCoverUrl, '/');
}
$trackImageUrl = $metaCoverUrl;
}
}
}
// Try to find image file by task_id pattern
if (empty($trackImageUrl) && !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);
$trackImageUrl = '/uploads/track_covers/' . basename($mostRecent);
}
}
}
}
if (empty($trackImageUrl)) {
$trackImageUrl = '/generate_waveform_og.php?track=' . urlencode((string)$track_id);
}
// Update track array with normalized image URL
$track['image_url'] = $trackImageUrl;
// Helper function to normalize track image URL (same logic as above)
function normalizeTrackImageUrl($track) {
$trackImageUrl = $track['image_url'] ?? null;
// If image_url exists and is not empty/null, use it (normalize local paths)
if (!empty($trackImageUrl) && $trackImageUrl !== 'null' && $trackImageUrl !== 'NULL') {
// Normalize local paths (not external URLs)
if (!preg_match('/^https?:\/\//', $trackImageUrl)) {
if (!str_starts_with($trackImageUrl, '/')) {
$trackImageUrl = '/' . ltrim($trackImageUrl, '/');
}
}
// If it's external, use it as-is (original image from database)
} else {
// Only if image_url is empty/null, try fallback sources
if (!empty($track['metadata'])) {
$metadata = is_string($track['metadata']) ? json_decode($track['metadata'], true) : $track['metadata'];
if (isset($metadata['image_url']) && !empty($metadata['image_url'])) {
$metaImageUrl = $metadata['image_url'];
if (strpos($metaImageUrl, 'http://') !== 0 && strpos($metaImageUrl, 'https://') !== 0) {
if (!str_starts_with($metaImageUrl, '/')) {
$metaImageUrl = '/' . ltrim($metaImageUrl, '/');
}
$trackImageUrl = $metaImageUrl;
}
} elseif (isset($metadata['cover_url']) && !empty($metadata['cover_url'])) {
$metaCoverUrl = $metadata['cover_url'];
if (strpos($metaCoverUrl, 'http://') !== 0 && strpos($metaCoverUrl, 'https://') !== 0) {
if (!str_starts_with($metaCoverUrl, '/')) {
$metaCoverUrl = '/' . ltrim($metaCoverUrl, '/');
}
$trackImageUrl = $metaCoverUrl;
}
}
}
// Try to find image file by task_id pattern
if (empty($trackImageUrl) && !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);
$trackImageUrl = '/uploads/track_covers/' . basename($mostRecent);
}
}
}
}
if (empty($trackImageUrl)) {
$trackId = $track['id'] ?? null;
if ($trackId) {
$trackImageUrl = '/generate_waveform_og.php?track=' . urlencode((string)$trackId);
}
}
return $trackImageUrl;
}
function formatDurationToISO8601($seconds) {
$seconds = max(0, (int)$seconds);
$hours = (int) floor($seconds / 3600);
$minutes = (int) floor(($seconds % 3600) / 60);
$remainingSeconds = $seconds % 60;
$interval = 'PT';
if ($hours > 0) {
$interval .= $hours . 'H';
}
if ($minutes > 0) {
$interval .= $minutes . 'M';
}
if ($remainingSeconds > 0 || $interval === 'PT') {
$interval .= $remainingSeconds . 'S';
}
return $interval;
}
// Convert image URL to absolute URL for Open Graph (required by Facebook, Twitter, etc.)
$base_url = 'https://soundstudiopro.com';
// Use the normalized trackImageUrl that was set earlier
$final_image_url = $trackImageUrl ?? $track['image_url'] ?? null;
if (!empty($final_image_url) && $final_image_url !== 'null' && $final_image_url !== 'NULL') {
// If it's already an absolute URL, use it as-is
if (strpos($final_image_url, 'http://') === 0 || strpos($final_image_url, 'https://') === 0) {
$page_image = $final_image_url;
} else {
// Convert relative path to absolute URL
// Remove leading slash if present, then add it back to ensure proper URL construction
$clean_path = ltrim($final_image_url, '/');
$page_image = $base_url . '/' . $clean_path;
// Verify the image file exists and is accessible (for local files)
if (strpos($clean_path, 'uploads/') === 0 || strpos($clean_path, '/uploads/') === 0) {
$local_path = $_SERVER['DOCUMENT_ROOT'] . '/' . ltrim($clean_path, '/');
if (!file_exists($local_path)) {
// Image file doesn't exist, fallback to generated waveform
$page_image = $base_url . '/generate_waveform_og.php?track=' . urlencode((string)$track_id);
}
}
}
} else {
// Fallback to generated waveform OG image for guaranteed uniqueness
// Note: generate_waveform_og.php doesn't need share token - it queries public track data
$page_image = $base_url . '/generate_waveform_og.php?track=' . urlencode((string)$track_id);
}
// Ensure OG image URL is always absolute and accessible
// Facebook requires absolute URLs with https://
if (strpos($page_image, 'http://') !== 0 && strpos($page_image, 'https://') !== 0) {
$page_image = $base_url . '/' . ltrim($page_image, '/');
}
// Force https:// for OG images (Facebook prefers https)
if (strpos($page_image, 'http://') === 0) {
$page_image = str_replace('http://', 'https://', $page_image);
}
// Debug: Log OG image URL for troubleshooting (only in development)
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("OG Image URL for track $track_id: $page_image");
}
// Ensure the image URL is absolute (don't HTML encode URLs in meta tags)
// The URL will be properly escaped in header.php when output
$track_duration = gmdate("i:s", $track['duration'] ?? 180);
$release_date = date('Y-m-d', strtotime($track['created_at']));
// Set variables for header.php
$og_type = 'music.song';
$og_url = $page_url;
$og_title = $page_title;
$og_description = $page_description;
$og_image = $page_image;
$twitter_title = $page_title;
$twitter_description = $page_description;
$twitter_image = $page_image;
$track_url = $page_url;
$track_artist = htmlspecialchars($track['artist_name']);
$canonical_url = $page_url;
$current_page = 'track';
// Build SEO keywords for better search engine indexing
// Include track name, artist name, and relevant music terms
// Use language-appropriate terms while keeping technical terms in English (common search terms)
$seo_lang = getCurrentLanguage();
$seo_keywords = [
$track_title_clean,
$artist_name_clean,
'SoundStudioPro'
];
// Add language-appropriate keywords
if ($seo_lang === 'fr') {
$seo_keywords = array_merge($seo_keywords, [
'musique IA',
'musique gΓ©nΓ©rΓ©e par IA',
'musique libre de droits',
'production musicale',
'musique Γ©lectronique',
'piste musicale',
'AI music', // Also include English terms (common searches)
'AI-generated music',
'royalty-free music'
]);
} else {
$seo_keywords = array_merge($seo_keywords, [
'AI music',
'AI-generated music',
'royalty-free music',
'music production',
'electronic music',
'music track'
]);
}
// Add BPM if available (important for DJ searches)
if (!empty($bpm) && $bpm > 0) {
$seo_keywords[] = 'BPM ' . round($bpm);
$seo_keywords[] = round($bpm) . ' BPM';
}
// Add musical key if available (important for DJ mixing)
if (!empty($key)) {
$seo_keywords[] = 'Key ' . $key;
$seo_keywords[] = $key . ' key';
}
// Add Camelot key if available (very important for DJs)
if (!empty($camelot)) {
$seo_keywords[] = 'Camelot ' . $camelot;
$seo_keywords[] = $camelot . ' Camelot';
// Also add without "Camelot" for shorter searches
$seo_keywords[] = $camelot;
}
// Add genre if available
if (!empty($genre)) {
$seo_keywords[] = $genre;
}
// Add tags if available
if (!empty($track['tags'])) {
$tags = explode(',', $track['tags']);
foreach ($tags as $tag) {
$cleanTag = trim($tag);
if (!empty($cleanTag) && strlen($cleanTag) > 2) {
$seo_keywords[] = $cleanTag;
}
}
}
// Remove duplicates and limit to reasonable number
$seo_keywords = array_slice(array_unique($seo_keywords), 0, 15);
$additional_meta = [
'keywords' => implode(', ', $seo_keywords),
'author' => $artist_name_clean
];
// Include header (which outputs DOCTYPE, html, head, and opens body)
include 'includes/header.php';
if (!$isAjaxRequest) {
// Use signed proxy URL for schema.org to hide direct audio file paths
$absoluteAudioUrl = 'https://soundstudiopro.com' . getSignedAudioUrl($track['id']);
$audioEncoding = 'audio/mpeg';
if (!empty($absoluteAudioUrl)) {
$extension = strtolower(pathinfo(parse_url($absoluteAudioUrl, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION));
if ($extension === 'wav') {
$audioEncoding = 'audio/wav';
} elseif ($extension === 'aac') {
$audioEncoding = 'audio/aac';
} elseif ($extension === 'flac') {
$audioEncoding = 'audio/flac';
}
}
$isoDuration = formatDurationToISO8601((int)($track['duration'] ?? 0));
$schemaKeywords = [];
if (!empty($track['tags'])) {
foreach (explode(',', $track['tags']) as $tag) {
$cleanTag = trim($tag);
if ($cleanTag !== '') {
$schemaKeywords[] = $cleanTag;
}
}
}
if (!empty($metadata['tags']) && is_array($metadata['tags'])) {
foreach ($metadata['tags'] as $tag) {
$cleanTag = trim((string)$tag);
if ($cleanTag !== '') {
$schemaKeywords[] = $cleanTag;
}
}
}
// Remove duplicate keywords
$schemaKeywords = array_values(array_unique($schemaKeywords));
$trackSchema = [
'@context' => 'https://schema.org',
'@type' => 'MusicRecording',
'name' => $display_title ?? $track['title'],
'alternateName' => $track_title_clean, // Add alternate name for better search matching
'description' => $page_description,
'url' => $page_url,
'image' => $page_image,
'duration' => $isoDuration,
'datePublished' => $release_date,
'genre' => $genre,
'byArtist' => [
'@type' => 'MusicGroup',
'name' => $track['artist_name'],
'alternateName' => $artist_name_clean, // Add alternate name for better search matching
'url' => 'https://soundstudiopro.com/artist_profile.php?id=' . $track['user_id']
],
'audio' => [
'@type' => 'AudioObject',
'contentUrl' => $absoluteAudioUrl,
'encodingFormat' => $audioEncoding,
'duration' => $isoDuration,
'name' => $display_title ?? $track['title'],
'description' => $page_description,
'license' => 'https://soundstudiopro.com/commercial_license.php'
],
'publisher' => [
'@type' => 'Organization',
'name' => 'SoundStudioPro',
'url' => 'https://soundstudiopro.com'
]
];
// Add BPM if available (important for DJ searches and music matching)
if (!empty($bpm) && $bpm > 0) {
$trackSchema['additionalProperty'] = $trackSchema['additionalProperty'] ?? [];
$trackSchema['additionalProperty'][] = [
'@type' => 'PropertyValue',
'name' => 'BPM',
'value' => round($bpm)
];
}
// Add musical key if available (important for DJ mixing)
if (!empty($key)) {
$trackSchema['additionalProperty'] = $trackSchema['additionalProperty'] ?? [];
$trackSchema['additionalProperty'][] = [
'@type' => 'PropertyValue',
'name' => 'Musical Key',
'value' => $key
];
}
// Add Camelot key if available (very important for DJs - mixing compatibility)
if (!empty($camelot)) {
$trackSchema['additionalProperty'] = $trackSchema['additionalProperty'] ?? [];
$trackSchema['additionalProperty'][] = [
'@type' => 'PropertyValue',
'name' => 'Camelot Key',
'value' => $camelot
];
}
if (!empty($schemaKeywords)) {
$trackSchema['keywords'] = implode(', ', $schemaKeywords);
}
echo '<script type="application/ld+json">' . json_encode($trackSchema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . '</script>';
}
?>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%);
color: #ffffff;
line-height: 1.6;
overflow-x: hidden;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 0 20px;
position: relative;
z-index: 1;
}
/* Track Hero Section */
.track-hero {
padding: 30px 20px 20px;
text-align: center;
max-width: 900px;
margin: 0 auto;
}
.track-cover {
width: 450px;
height: 450px;
max-width: 90vw;
max-height: 90vw;
margin: 0 auto 1.5rem;
border-radius: 24px;
overflow: hidden;
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(255, 255, 255, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
position: relative;
transition: transform 0.3s ease, box-shadow 0.3s ease;
animation: floatGlow 4s ease-in-out infinite;
}
.track-cover::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(
circle at center,
rgba(102, 126, 234, 0.4) 0%,
rgba(118, 75, 162, 0.3) 30%,
transparent 70%
);
animation: innerGlow 3s ease-in-out infinite;
pointer-events: none;
z-index: 1;
border-radius: 50%;
opacity: 0.6;
}
.track-cover::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
135deg,
rgba(102, 126, 234, 0.15) 0%,
transparent 50%,
rgba(118, 75, 162, 0.15) 100%
);
animation: shimmer 5s ease-in-out infinite;
pointer-events: none;
z-index: 2;
border-radius: 24px;
mix-blend-mode: overlay;
}
.track-cover:hover {
transform: translateY(-5px) scale(1.02);
box-shadow:
0 35px 70px rgba(0, 0, 0, 0.6),
0 0 0 1px rgba(102, 126, 234, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 0 60px rgba(102, 126, 234, 0.4),
inset 0 0 80px rgba(102, 126, 234, 0.2);
animation: floatGlowHover 2s ease-in-out infinite;
}
.track-cover img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
position: relative;
z-index: 0;
filter: brightness(1.05) contrast(1.02);
animation: imagePulse 4s ease-in-out infinite;
}
/* Floating and glowing animation */
@keyframes floatGlow {
0%, 100% {
transform: translateY(0px) scale(1);
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(255, 255, 255, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.1),
0 0 40px rgba(102, 126, 234, 0.3),
inset 0 0 60px rgba(102, 126, 234, 0.15);
}
50% {
transform: translateY(-8px) scale(1.01);
box-shadow:
0 30px 60px rgba(0, 0, 0, 0.6),
0 0 0 1px rgba(255, 255, 255, 0.15),
inset 0 1px 0 rgba(255, 255, 255, 0.15),
0 0 60px rgba(102, 126, 234, 0.5),
inset 0 0 80px rgba(102, 126, 234, 0.25);
}
}
@keyframes floatGlowHover {
0%, 100% {
transform: translateY(-5px) scale(1.02);
}
50% {
transform: translateY(-10px) scale(1.025);
}
}
/* Inner glow animation */
@keyframes innerGlow {
0%, 100% {
transform: translate(-50%, -50%) scale(0.8);
opacity: 0.4;
}
50% {
transform: translate(-50%, -50%) scale(1.2);
opacity: 0.8;
}
}
/* Shimmer effect */
@keyframes shimmer {
0%, 100% {
opacity: 0.3;
transform: translateX(-100%) translateY(-100%);
}
50% {
opacity: 0.6;
transform: translateX(100%) translateY(100%);
}
}
/* Image pulse for subtle movement */
@keyframes imagePulse {
0%, 100% {
transform: scale(1);
filter: brightness(1.05) contrast(1.02);
}
50% {
transform: scale(1.02);
filter: brightness(1.1) contrast(1.05);
}
}
/* Scoped to track-hero section only, not global player */
.track-hero .track-title {
font-size: clamp(2rem, 4vw, 3.5rem);
font-weight: 900;
margin-bottom: 0.75rem;
line-height: 1.2;
background: linear-gradient(135deg, #ffffff 0%, #e0e7ff 50%, #667eea 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: -0.02em;
text-shadow: 0 0 40px rgba(102, 126, 234, 0.3);
padding: 0 1rem;
}
.track-hero .track-artist {
font-size: clamp(1.75rem, 3.5vw, 3rem);
color: #cbd5e0;
margin-bottom: 0.75rem;
font-weight: 600;
padding: 0 1rem;
min-height: 44px; /* Minimum touch target size for mobile */
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 0.5rem;
}
.track-hero .track-artist .artist-link {
color: #a5b4fc;
text-decoration: none;
font-weight: 700;
transition: all 0.3s ease;
position: relative;
padding: 0.5rem 1rem;
border-radius: 12px;
display: inline-block;
min-height: 44px;
line-height: 44px;
touch-action: manipulation;
-webkit-tap-highlight-color: rgba(165, 180, 252, 0.3);
}
.track-hero .track-artist .artist-link:active {
background: rgba(165, 180, 252, 0.2);
transform: scale(0.98);
}
.track-hero .track-artist .artist-link:hover {
color: #818cf8;
text-shadow: 0 0 20px rgba(129, 140, 248, 0.5);
}
.track-hero .track-artist .artist-link::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: linear-gradient(90deg, #667eea, #818cf8);
transition: width 0.3s ease;
}
.track-hero .track-artist .artist-link:hover::after {
width: 100%;
}
.artist-link {
color: #667eea;
text-decoration: none;
font-weight: 600;
}
.track-prompt {
color: #718096;
font-size: 0.9rem;
max-width: 500px;
margin: 0 auto 1rem;
}
/* Audio Player - removed, now using compact player */
/* Play button as action button */
.play-action-btn {
background: linear-gradient(135deg, #667eea, #764ba2);
min-width: 120px;
}
.play-action-btn.playing {
background: linear-gradient(135deg, #48bb78, #38a169);
}
.play-action-btn #playText {
display: inline;
}
.play-action-btn.playing #playText {
display: none;
}
/* Beautiful Time Display */
.track-time-display {
text-align: center;
margin: 0.75rem 0 1rem;
font-family: 'Inter', sans-serif;
}
.track-time-display span {
display: inline-block;
}
.track-time-display #currentTime {
font-size: clamp(2rem, 4vw, 3.5rem);
font-weight: 700;
background: linear-gradient(135deg, #ffffff 0%, #e0e7ff 50%, #667eea 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: -0.02em;
margin-right: 0.5rem;
}
.track-time-display .time-separator {
font-size: clamp(1.5rem, 3vw, 2.5rem);
color: rgba(255, 255, 255, 0.4);
margin: 0 0.75rem;
font-weight: 300;
}
.track-time-display #totalTime {
font-size: clamp(1.75rem, 3.5vw, 3rem);
font-weight: 600;
color: #cbd5e0;
letter-spacing: -0.01em;
}
/* Compact player controls */
.compact-player {
max-width: 600px;
margin: 0.75rem auto 1rem;
padding: 0 1rem;
}
/* Notification animations */
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
.time-display {
font-size: 0.9rem;
color: #a0aec0;
min-width: 100px;
text-align: center;
}
.progress-container {
width: 100%;
height: 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
cursor: pointer;
position: relative;
margin: 0.5rem 0;
touch-action: manipulation;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 3px;
width: 0%;
transition: width 0.1s ease;
}
.volume-control {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 1rem;
pointer-events: auto;
position: relative;
z-index: 10;
}
.volume-slider {
width: 100px;
height: 4px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
outline: none;
cursor: pointer;
touch-action: manipulation;
-webkit-appearance: none;
appearance: none;
pointer-events: auto;
z-index: 10;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 12px;
height: 12px;
background: #667eea;
border-radius: 50%;
cursor: pointer;
pointer-events: auto;
}
.volume-slider::-moz-range-thumb {
width: 12px;
height: 12px;
background: #667eea;
border-radius: 50%;
cursor: pointer;
border: none;
pointer-events: auto;
}
/* Track Stats */
.track-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 0.75rem;
margin: 1.5rem 0;
}
.stat-item {
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 1rem;
text-align: center;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.stat-value {
display: block;
font-size: 1.2rem;
font-weight: 700;
color: #667eea;
margin-bottom: 0.3rem;
}
.stat-label {
font-size: 0.9rem;
color: #a0aec0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Track Rankings Section */
.track-rankings {
margin: 1.5rem 0;
}
.rankings-card {
background: linear-gradient(135deg, rgba(251, 191, 36, 0.1) 0%, rgba(245, 158, 11, 0.1) 100%);
border-radius: 15px;
padding: 1.5rem;
border: 2px solid rgba(251, 191, 36, 0.3);
backdrop-filter: blur(10px);
}
.rankings-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.rankings-header i {
font-size: 1.5rem;
color: #fbbf24;
}
.rankings-header h3 {
margin: 0;
font-size: 1.3rem;
font-weight: 600;
color: #ffffff;
}
.rankings-content {
display: flex;
flex-direction: column;
gap: 1rem;
}
.ranking-main {
text-align: center;
padding: 1rem;
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
}
.overall-rank {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.rank-label {
font-size: 0.9rem;
color: #a0aec0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.rank-value {
font-size: 3rem;
font-weight: 700;
color: #fbbf24;
line-height: 1;
}
.rank-out-of {
font-size: 1rem;
color: #a0aec0;
}
.rank-percentile {
margin-top: 0.5rem;
}
.percentile-badge {
display: inline-block;
padding: 0.5rem 1rem;
background: linear-gradient(135deg, #fbbf24, #f59e0b);
color: #000000;
border-radius: 20px;
font-weight: 600;
font-size: 0.9rem;
}
.ranking-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
}
.ranking-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: rgba(0, 0, 0, 0.2);
border-radius: 10px;
border-left: 3px solid #fbbf24;
}
.ranking-item i {
font-size: 1.5rem;
color: #fbbf24;
}
.ranking-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
flex: 1;
}
.ranking-metric {
font-size: 0.85rem;
color: #a0aec0;
}
.ranking-position {
font-size: 1.2rem;
font-weight: 600;
color: #ffffff;
}
@media (max-width: 768px) {
.rank-value {
font-size: 2.5rem;
}
.ranking-details {
grid-template-columns: 1fr;
}
}
/* Copyright Section */
.copyright-section {
margin: 1.5rem 0;
}
.copyright-card {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(240, 147, 251, 0.1) 100%);
border-radius: 15px;
padding: 1.5rem;
border: 2px solid rgba(102, 126, 234, 0.3);
backdrop-filter: blur(10px);
}
.copyright-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.copyright-header i {
font-size: 1.5rem;
color: #667eea;
}
.copyright-header h3 {
margin: 0;
font-size: 1.3rem;
font-weight: 600;
color: #ffffff;
}
.copyright-content {
display: flex;
flex-direction: column;
gap: 1rem;
}
.copyright-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
border-left: 3px solid #667eea;
}
.copyright-label {
font-weight: 600;
color: #a0aec0;
font-size: 0.95rem;
}
.copyright-value {
font-weight: 500;
color: #ffffff;
font-size: 1rem;
font-family: 'Courier New', monospace;
}
.copyright-seal-container {
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: center;
align-items: center;
gap: 1.5rem;
flex-wrap: wrap;
}
.ssp-copyright-seal {
display: flex;
align-items: center;
gap: 1rem;
padding: 1.25rem 1.75rem;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%);
border: 2px solid rgba(102, 126, 234, 0.4);
border-radius: 20px;
backdrop-filter: blur(10px);
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.2);
transition: all 0.3s ease;
position: relative;
}
.ssp-copyright-seal:hover {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.25) 0%, rgba(118, 75, 162, 0.25) 100%);
border-color: rgba(102, 126, 234, 0.6);
box-shadow: 0 6px 25px rgba(102, 126, 234, 0.3);
transform: translateY(-2px);
}
.seal-report-hint {
display: flex;
align-items: center;
gap: 0.5rem;
margin-left: auto;
padding-left: 1rem;
border-left: 1px solid rgba(255, 255, 255, 0.2);
font-size: 0.8rem;
color: #fca5a5;
font-weight: 500;
opacity: 0.8;
transition: opacity 0.3s ease;
}
.ssp-copyright-seal:hover .seal-report-hint {
opacity: 1;
}
.seal-report-hint i {
font-size: 0.9rem;
}
.seal-icon {
width: 50px;
height: 50px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
flex-shrink: 0;
}
.seal-icon i {
font-size: 1.5rem;
color: #ffffff;
text-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.seal-content {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.seal-brand {
font-size: 1.1rem;
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing: 0.5px;
}
.seal-text {
font-size: 0.85rem;
color: #a0aec0;
font-weight: 500;
letter-spacing: 0.3px;
}
/* Copyright Certificate */
.copyright-certificate {
margin-top: 2rem;
padding: 1.5rem;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.08) 0%, rgba(118, 75, 162, 0.08) 100%);
border: 1px solid rgba(102, 126, 234, 0.2);
border-radius: 12px;
border-left: 4px solid #667eea;
}
.certificate-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.25rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.certificate-header i {
font-size: 1.5rem;
color: #667eea;
}
.certificate-header h4 {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: #ffffff;
}
.certificate-content {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.certificate-field {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.cert-label {
font-size: 0.85rem;
color: #a0aec0;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.cert-value {
font-size: 1rem;
color: #ffffff;
font-weight: 500;
}
@media (max-width: 768px) {
.copyright-item {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.copyright-value {
font-size: 0.9rem;
}
.copyright-seal-container {
flex-direction: column;
gap: 1rem;
}
.ssp-copyright-seal {
padding: 1rem 1.25rem;
gap: 0.75rem;
flex-wrap: wrap;
}
.seal-icon {
width: 40px;
height: 40px;
}
.seal-icon i {
font-size: 1.2rem;
}
.seal-brand {
font-size: 0.95rem;
}
.seal-text {
font-size: 0.75rem;
}
.seal-report-hint {
margin-left: 0;
padding-left: 0;
border-left: none;
border-top: 1px solid rgba(255, 255, 255, 0.2);
padding-top: 0.75rem;
margin-top: 0.75rem;
width: 100%;
justify-content: center;
}
}
/* Track Info Grid */
.track-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 0.8rem;
margin: 1.5rem 0;
}
.info-card {
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 1rem;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.info-label {
font-size: 0.9rem;
color: #a0aec0;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 0.5rem;
}
.info-value {
font-size: 1rem;
font-weight: 600;
color: #ffffff;
}
/* Action Buttons */
.action-buttons {
display: flex;
gap: 0.8rem;
justify-content: center;
flex-wrap: wrap;
margin: 1rem 0;
position: relative;
z-index: 10;
pointer-events: auto;
}
.action-btn {
padding: 0.6rem 1.2rem;
border-radius: 20px;
border: none;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 0.4rem;
text-decoration: none;
color: white;
touch-action: manipulation;
min-height: 44px;
position: relative;
z-index: 11;
pointer-events: auto;
}
.primary-btn {
background: linear-gradient(135deg, #667eea, #764ba2);
}
.secondary-btn {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.like-btn {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.like-btn.liked {
background: linear-gradient(135deg, #e53e3e, #c53030);
}
.action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
}
/* Comments Section */
.comments-section {
margin: 1.5rem 0;
}
.section-title {
font-size: 1.3rem;
font-weight: 700;
margin-bottom: 0.75rem;
text-align: center;
}
.comment-form {
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
padding: 1.25rem;
margin-bottom: 1.5rem;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.comment-input {
width: 100%;
min-height: 100px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 10px;
padding: 1rem;
color: white;
font-family: inherit;
resize: vertical;
}
.comment-input::placeholder {
color: #a0aec0;
}
.comments-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.comment {
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
padding: 1.5rem;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.comment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.comment-author {
font-weight: 600;
color: #667eea;
}
.comment-author-link {
font-weight: 600;
color: #667eea;
text-decoration: none;
transition: all 0.3s ease;
}
.comment-author-link:hover {
color: #764ba2;
text-decoration: underline;
}
.comment-date {
font-size: 0.9rem;
color: #a0aec0;
}
.comment-text {
color: #e2e8f0;
line-height: 1.6;
}
/* Related Tracks */
.related-tracks {
margin: 3rem 0;
}
/* Variations Section */
.variations-section {
margin: 3rem 0;
}
.variations-container {
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
padding: 1.5rem;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.variations-grid {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.variation-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
}
.variation-item:hover {
background: rgba(255, 255, 255, 0.08);
transform: translateX(5px);
}
.variation-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
flex: 1;
}
.variation-title {
font-weight: 600;
color: #ffffff;
font-size: 0.95rem;
}
.variation-duration {
font-size: 0.85rem;
color: #a0aec0;
}
.variation-play-btn {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
flex-shrink: 0;
}
.variation-play-btn:hover {
transform: scale(1.1);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.variation-play-btn i {
font-size: 0.9rem;
}
/* Lyrics Section */
.lyrics-section {
margin: 3rem 0;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.05) 0%, rgba(240, 147, 251, 0.05) 100%);
border-radius: 20px;
padding: 2rem;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.lyrics-container {
position: relative;
}
.lyrics-content {
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
padding: 2rem;
border: 1px solid rgba(255, 255, 255, 0.1);
max-height: 300px;
overflow: hidden;
transition: max-height 0.3s ease;
}
.lyrics-content.expanded {
max-height: none;
}
.lyrics-text {
font-size: 1.1rem;
line-height: 1.8;
color: #e0e0e0;
font-family: 'Inter', sans-serif;
text-align: center;
}
.lyrics-toggle-btn {
position: absolute;
bottom: -15px;
right: 20px;
background: linear-gradient(135deg, #667eea 0%, #f093fb 100%);
color: white;
border: none;
padding: 10px 20px;
border-radius: 25px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.lyrics-toggle-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
.lyrics-toggle-btn.expanded {
background: linear-gradient(135deg, #f093fb 0%, #667eea 100%);
}
.lyrics-toggle-btn i {
transition: transform 0.3s ease;
}
.lyrics-toggle-btn.expanded i {
transform: rotate(180deg);
}
/* Toast Animation Keyframes */
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
.tracks-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.track-card {
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
overflow: hidden;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
cursor: pointer;
}
.track-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.track-card img {
width: 100%;
height: 150px;
object-fit: cover;
}
.track-card-content {
padding: 1rem;
}
.track-card-title {
font-weight: 600;
margin-bottom: 0.5rem;
color: #ffffff;
}
.track-card-artist {
font-size: 0.9rem;
color: #a0aec0;
}
/* Responsive Design */
@media (max-width: 768px) {
.container {
padding: 0 10px;
}
.nav {
padding: 0.5rem 0;
}
.back-btn {
font-size: 0.9rem;
gap: 0.3rem;
}
.logo-text {
font-size: 1.8rem;
}
.logo-icon {
font-size: 2.2rem;
}
.track-hero {
padding: 40px 20px 30px;
}
.track-cover {
width: 85vw;
height: 85vw;
max-width: 400px;
max-height: 400px;
margin-bottom: 2rem;
}
.track-hero .track-title {
font-size: 2rem;
margin-bottom: 1.2rem;
}
.track-hero .track-artist {
font-size: 1.75rem;
margin-bottom: 1.2rem;
}
.track-hero .track-artist .artist-link {
padding: 0.75rem 1.25rem;
min-height: 48px;
line-height: 48px;
}
.track-prompt {
font-size: 0.8rem;
max-width: 100%;
margin-bottom: 0.8rem;
}
.audio-player {
padding: 1rem;
margin: 0.8rem 0;
}
.player-controls {
gap: 0.8rem;
margin-bottom: 0.8rem;
}
.play-btn {
width: 45px;
height: 45px;
font-size: 1rem;
}
.time-display {
font-size: 0.8rem;
min-width: 80px;
}
.progress-container {
margin: 0.8rem 0;
}
.volume-control {
margin-top: 0.8rem;
}
.volume-slider {
width: 80px;
}
.track-stats {
grid-template-columns: repeat(2, 1fr);
gap: 0.6rem;
margin: 0.8rem 0;
}
.stat-item {
padding: 0.8rem;
border-radius: 10px;
}
.stat-value {
font-size: 1rem;
margin-bottom: 0.2rem;
}
.stat-label {
font-size: 0.8rem;
}
.track-info {
grid-template-columns: repeat(2, 1fr);
gap: 0.6rem;
margin: 0.8rem 0;
}
.info-card {
padding: 0.8rem;
border-radius: 10px;
}
.info-label {
font-size: 0.8rem;
margin-bottom: 0.3rem;
}
.info-value {
font-size: 0.9rem;
}
.action-buttons {
flex-direction: column;
align-items: center;
gap: 0.6rem;
margin: 0.8rem 0;
}
.action-btn {
width: 100%;
max-width: 280px;
justify-content: center;
padding: 0.8rem 1rem;
font-size: 0.9rem;
}
.comments-section {
margin: 1rem 0;
}
.section-title {
font-size: 1.3rem;
margin-bottom: 1rem;
}
.comment-form {
padding: 1rem;
margin-bottom: 1rem;
}
.comment-input {
min-height: 80px;
padding: 0.8rem;
}
.comment {
padding: 1rem;
}
.comment-header {
margin-bottom: 0.3rem;
}
.comment-author {
font-size: 0.9rem;
}
.comment-date {
font-size: 0.8rem;
}
.comment-text {
font-size: 0.9rem;
}
.related-tracks {
margin: 1rem 0;
}
.tracks-grid {
grid-template-columns: 1fr;
gap: 1rem;
margin-top: 1rem;
}
.track-card {
border-radius: 12px;
}
.track-card img {
height: 120px;
}
.track-card-content {
padding: 0.8rem;
}
.track-card-title {
font-size: 0.9rem;
margin-bottom: 0.3rem;
}
.track-card-artist {
font-size: 0.8rem;
}
}
@media (max-width: 480px) {
.container {
padding: 0 8px;
}
.track-cover {
width: 180px;
height: 180px;
}
.track-hero .track-title {
font-size: 1.4rem;
}
.track-hero .track-artist {
font-size: 0.8rem;
}
.track-prompt {
font-size: 0.75rem;
}
.track-stats {
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
}
.stat-item {
padding: 0.6rem;
}
.stat-value {
font-size: 0.9rem;
}
.stat-label {
font-size: 0.7rem;
}
.track-info {
grid-template-columns: 1fr;
gap: 0.5rem;
}
.info-card {
padding: 0.6rem;
}
.info-label {
font-size: 0.7rem;
}
.info-value {
font-size: 0.8rem;
}
.action-btn {
max-width: 260px;
padding: 0.7rem 0.9rem;
font-size: 0.85rem;
}
.play-btn {
width: 40px;
height: 40px;
font-size: 0.9rem;
}
.time-display {
font-size: 0.75rem;
min-width: 70px;
}
.volume-slider {
width: 70px;
}
.comment-input {
min-height: 70px;
padding: 0.6rem;
}
.comment {
padding: 0.8rem;
}
.comment-text {
font-size: 0.85rem;
}
.track-card img {
height: 100px;
}
.track-card-content {
padding: 0.6rem;
}
.track-card-title {
font-size: 0.85rem;
}
.track-card-artist {
font-size: 0.75rem;
}
}
/* Loading States */
.loading {
opacity: 0.6;
pointer-events: none;
}
/* Camelot Key Badge */
.camelot-key {
display: inline-block;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 700;
margin-left: 8px;
vertical-align: middle;
letter-spacing: 0.5px;
}
/* Analyzing States */
.analyzing {
color: #667eea !important;
position: relative;
}
.analyzing-icon {
color: #667eea;
}
.info-label {
display: flex;
align-items: center;
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in {
animation: fadeIn 0.6s ease-out;
}
/* Enhanced Metadata Styles */
.enhanced-metadata .metadata-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.enhanced-metadata .metadata-card {
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 1rem;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
text-align: center;
}
.enhanced-metadata .metadata-label {
font-size: 0.9rem;
color: #a0aec0;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 0.5rem;
}
.enhanced-metadata .metadata-value {
font-size: 1.2rem;
font-weight: 700;
color: #667eea;
}
.enhanced-metadata .quality-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.enhanced-metadata .cost-card {
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
color: white;
}
.enhanced-metadata .status-processing {
color: #f59e0b; /* Amber for processing */
}
.enhanced-metadata .status-complete {
color: #48bb78; /* Green for complete */
}
.enhanced-metadata .status-failed {
color: #e53e3e; /* Red for failed */
}
/* Processing Info Styles */
.processing-info .metadata-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.processing-info .metadata-card {
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 1rem;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
text-align: center;
}
.processing-info .analysis-subtitle {
font-size: 1.1rem;
color: #888;
margin-bottom: 0.8rem;
}
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
position: relative;
overflow: hidden;
}
/* Spectrum and Segment Bars */
.spectrum-bar {
position: absolute;
bottom: 0;
border-radius: 2px;
transition: height 0.1s ease;
}
.segment-item {
position: absolute;
top: 50%;
transform: translateY(-50%);
border-radius: 8px;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.segment-item:hover {
transform: translateY(-50%) scale(1.05);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
}
/* Quality Score Styles */
.quality-score {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.score-number {
font-size: 1.5rem;
font-weight: 800;
}
.score-bar {
width: 100%;
height: 8px;
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
overflow: hidden;
}
.score-fill {
height: 100%;
background: linear-gradient(90deg, #48bb78, #38a169);
border-radius: 4px;
transition: width 0.3s ease;
}
/* Section Description */
.section-description {
color: #a0aec0;
text-align: center;
margin-bottom: 1.5rem;
font-size: 0.95rem;
}
/* Related Tracks Section */
.related-tracks {
margin-bottom: 6rem; /* Space for global player */
}
/* Related Tracks Grid */
.tracks-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.mini-track-card {
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 1rem;
transition: all 0.3s ease;
cursor: pointer;
}
.mini-track-card:hover {
background: rgba(102, 126, 234, 0.1);
transform: translateY(-2px);
}
.mini-track-title {
font-weight: 600;
margin-bottom: 0.5rem;
}
.mini-track-artist {
font-size: 0.9rem;
color: #a0aec0;
}
.track-cover-mini {
width: 100%;
height: 120px;
border-radius: 8px;
overflow: hidden;
margin-bottom: 0.75rem;
}
.track-cover-mini img {
width: 100%;
height: 100%;
object-fit: cover;
}
.genres-grid {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
justify-content: center;
max-width: 100%;
overflow: hidden;
}
.genre-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 10px 20px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
white-space: nowrap;
min-width: fit-content;
}
.genre-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,0.15);
}
/* Profile Edit Modal Styles (for ranking breakdown modal) */
.profile-edit-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
z-index: 10001;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.profile-edit-modal .modal-content {
background: #2a2a2a;
border-radius: 20px;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
border: 2px solid #667eea;
animation: slideUp 0.3s ease;
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
}
@keyframes slideUp {
from {
transform: translateY(50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.profile-edit-modal .modal-header {
background: #3a3a3a;
padding: 1.5rem 2rem;
border-bottom: 1px solid #4a4a4a;
display: flex;
justify-content: space-between;
align-items: center;
}
.profile-edit-modal .modal-header h3 {
color: white;
margin: 0;
font-size: 1.5rem;
font-weight: 600;
}
.profile-edit-modal .close-btn {
background: none;
border: none;
color: #a0a0a0;
font-size: 2rem;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.3s ease;
}
.profile-edit-modal .close-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.profile-edit-modal .modal-body {
padding: 2rem;
color: #e0e0e0;
}
.profile-edit-modal .modal-footer {
background: #3a3a3a;
padding: 1.5rem 2rem;
border-top: 1px solid #4a4a4a;
display: flex;
gap: 1rem;
justify-content: flex-end;
}
.profile-edit-modal .btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
}
.profile-edit-modal .btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
</style>
<div class="container">
<!-- Track Hero Section -->
<section class="track-hero fade-in">
<div class="track-cover">
<?php if (!empty($track['image_url'])): ?>
<img src="<?= htmlspecialchars($track['image_url']) ?>" alt="<?= htmlspecialchars(t('track.cover_alt'), ENT_QUOTES, 'UTF-8') ?>">
<?php else: ?>
<img src="/uploads/track_covers/track_45_1754191923.jpg" alt="<?= htmlspecialchars(t('track.default_cover_alt'), ENT_QUOTES, 'UTF-8') ?>">
<?php endif; ?>
</div>
<h1 class="track-title" id="trackTitle"><?= htmlspecialchars($display_title ?? ($track['title'] ?: t('track.untitled'))) ?></h1>
<div class="track-artist" id="trackArtist">
<?= t('track.by_prefix') ?> <a href="/artist_profile.php?id=<?= $track['user_id'] ?>" class="artist-link" id="artistLink"><?= htmlspecialchars($track['artist_name']) ?></a>
</div>
<!-- Beautiful Time Display -->
<div class="track-time-display fade-in">
<span id="currentTime">0:00</span>
<span class="time-separator">/</span>
<span id="totalTime"><?= $track_duration ?></span>
</div>
<!-- Prompt hidden - internal use only -->
</section>
<!-- Action Buttons -->
<section class="action-buttons fade-in">
<button class="action-btn play-action-btn" id="playBtn" onclick="togglePlay()">
<i class="fas fa-play" id="playIcon"></i>
<span id="playText"><?= t('track.play') ?></span>
</button>
<?php
// Check if track is private and accessed via share link - don't allow adding to cart
$isPrivateViaShare = ($track['is_public'] != 1 && $track['is_public'] !== null) && $hasValidShareToken && !($user_id && $track['user_id'] == $user_id);
?>
<?php if ($isPrivateViaShare): ?>
<button class="action-btn primary-btn" disabled style="opacity: 0.6; cursor: not-allowed;" title="<?= t('track.private_not_for_sale') ?>">
<i class="fas fa-lock"></i>
<?= t('track.private_not_for_sale') ?>
</button>
<?php else: ?>
<button class="action-btn primary-btn" id="addToCartBtn" data-track-id="<?= $track['id'] ?>" data-track-title="<?= htmlspecialchars($track['title'], ENT_QUOTES) ?>" data-track-price="<?= $track['price'] ?? 1.99 ?>">
<i class="fas fa-shopping-cart"></i>
<?= t('artist_profile.add_to_cart') ?>
</button>
<?php endif; ?>
<button class="action-btn like-btn <?= $track['user_liked'] ? 'liked' : '' ?>" onclick="toggleLike(<?= $track['id'] ?>, this)">
<i class="fas fa-heart"></i>
<?= $track['user_liked'] ? t('track.liked') : t('track.like') ?>
</button>
<button class="action-btn secondary-btn" onclick="shareCurrentTrack()">
<i class="fas fa-share"></i>
<?= t('track.share') ?>
</button>
<?php if ($user_id): ?>
<button class="action-btn secondary-btn" onclick="openAddToCrateModal(<?= $track['id'] ?>, <?= json_encode($track['title']) ?>)">
<i class="fas fa-box"></i>
<?= t('library.crates.add_to_crate') ?>
</button>
<?php endif; ?>
</section>
<!-- Progress Bar -->
<section class="compact-player fade-in">
<div class="progress-container" onclick="seekTo(event)">
<div class="progress-bar" id="progressBar"></div>
</div>
</section>
<!-- Track Stats -->
<section class="track-stats fade-in">
<div class="stat-item">
<span class="stat-value"><?= number_format($track['play_count']) ?></span>
<span class="stat-label"><?= t('track.stats.plays') ?></span>
</div>
<div class="stat-item">
<span class="stat-value"><?= number_format($track['like_count']) ?></span>
<span class="stat-label"><?= t('track.stats.likes') ?></span>
</div>
<div class="stat-item" style="cursor: pointer;" onclick="showTrackRatingsModal(<?= $track['id'] ?>, '<?= htmlspecialchars($track['title'], ENT_QUOTES) ?>')" title="<?= htmlspecialchars(t('track.stats.rating_tooltip')) ?>">
<span class="stat-value">
<i class="fas fa-star" style="color: #fbbf24; margin-right: 4px;"></i>
<?= number_format($track['average_rating'] ?? 0, 1) ?>/10
<?php if (($track['rating_count'] ?? 0) > 0): ?>
<span style="font-size: 0.9em; color: #a0aec0;">(<?= $track['rating_count'] ?>)</span>
<?php endif; ?>
</span>
<span class="stat-label"><?= t('track.stats.rating') ?></span>
</div>
<div class="stat-item">
<span class="stat-value"><?= $track_duration ?></span>
<span class="stat-label"><?= t('track.stats.duration') ?></span>
</div>
<div class="stat-item">
<span class="stat-value"><?= date('M j, Y', strtotime($track['created_at'])) ?></span>
<span class="stat-label"><?= t('track.stats.released') ?></span>
</div>
</section>
<!-- Track Rankings -->
<?php if ($track['status'] === 'complete' && !empty($rankings)): ?>
<section class="track-rankings fade-in">
<div class="rankings-card" style="cursor: pointer;" id="rankingsCard" onclick="openRankingBreakdown()">
<div class="rankings-header">
<i class="fas fa-trophy"></i>
<h3><?= t('track.rankings.title') ?></h3>
<i class="fas fa-info-circle" style="margin-left: auto; font-size: 1rem; color: #a0aec0; opacity: 0.7;"></i>
</div>
<div class="rankings-content">
<div class="ranking-main">
<div class="overall-rank">
<span class="rank-label"><?= t('track.rankings.overall') ?></span>
<span class="rank-value">#<?= number_format($rankings['overall']) ?></span>
<span class="rank-out-of"><?= t('track.rankings.of') ?> <?= number_format($rankings['total_tracks']) ?></span>
<div class="rank-percentile">
<span class="percentile-badge">Top <?= $rankings['overall_percentile'] ?>%</span>
</div>
</div>
</div>
<div class="ranking-details">
<div class="ranking-item">
<i class="fas fa-play-circle"></i>
<div class="ranking-info">
<span class="ranking-metric"><?= t('track.rankings.by_plays') ?></span>
<span class="ranking-position">#<?= number_format($rankings['plays']) ?> <?= t('track.rankings.of') ?> <?= number_format($rankings['total_tracks']) ?></span>
</div>
</div>
<div class="ranking-item">
<i class="fas fa-heart"></i>
<div class="ranking-info">
<span class="ranking-metric"><?= t('track.rankings.by_likes') ?></span>
<span class="ranking-position">#<?= number_format($rankings['likes']) ?> <?= t('track.rankings.of') ?> <?= number_format($rankings['total_tracks']) ?></span>
</div>
</div>
<?php if (($track['rating_count'] ?? 0) >= 3 && isset($rankings['rating'])): ?>
<div class="ranking-item">
<i class="fas fa-star"></i>
<div class="ranking-info">
<span class="ranking-metric"><?= t('track.rankings.by_rating') ?></span>
<span class="ranking-position">#<?= number_format($rankings['rating']) ?></span>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
</section>
<?php endif; ?>
<!-- Copyright & Release Information -->
<?php
$certificate = getCopyrightCertificate($track['id']);
?>
<section class="copyright-section fade-in">
<div class="copyright-card">
<div class="copyright-header">
<i class="fas fa-copyright"></i>
<h3><?= t('track.copyright.title') ?></h3>
</div>
<div class="copyright-content">
<div class="copyright-item">
<span class="copyright-label"><?= t('track.copyright.release_date') ?>:</span>
<span class="copyright-value"><?= date('F j, Y', strtotime($track['created_at'])) ?></span>
</div>
<div class="copyright-item">
<span class="copyright-label"><?= t('track.copyright.release_time') ?>:</span>
<span class="copyright-value"><?= date('g:i A T', strtotime($track['created_at'])) ?></span>
</div>
<div class="copyright-item">
<span class="copyright-label"><?= t('track.copyright.timestamp') ?>:</span>
<span class="copyright-value"><?= date('Y-m-d H:i:s', strtotime($track['created_at'])) ?> UTC</span>
</div>
<?php if ($certificate && $certificate['audio_hash']): ?>
<div class="copyright-item">
<span class="copyright-label"><?= t('track.copyright.audio_hash') ?>:</span>
<span class="copyright-value" style="font-family: 'Courier New', monospace; font-size: 0.85rem; word-break: break-all;"><?= htmlspecialchars($certificate['audio_hash']) ?></span>
</div>
<?php endif; ?>
<?php if ($certificate && $certificate['copyright_hash']): ?>
<div class="copyright-item">
<span class="copyright-label"><?= t('track.copyright.certificate_number') ?>:</span>
<span class="copyright-value" style="font-family: 'Courier New', monospace; font-weight: 600;"><?= htmlspecialchars($certificate['certificate_number']) ?></span>
</div>
<?php endif; ?>
</div>
<?php if ($certificate): ?>
<!-- Copyright Certificate -->
<div class="copyright-certificate">
<div class="certificate-header">
<i class="fas fa-certificate"></i>
<h4><?= t('track.copyright.certificate_title') ?></h4>
</div>
<div class="certificate-content">
<div class="certificate-field">
<span class="cert-label"><?= t('track.copyright.cert_track_id') ?>:</span>
<span class="cert-value">#<?= $certificate['track_id'] ?></span>
</div>
<div class="certificate-field">
<span class="cert-label"><?= t('track.copyright.cert_title') ?>:</span>
<span class="cert-value"><?= htmlspecialchars($certificate['title']) ?></span>
</div>
<div class="certificate-field">
<span class="cert-label"><?= t('track.copyright.cert_artist') ?>:</span>
<span class="cert-value"><?= htmlspecialchars($certificate['artist_name']) ?></span>
</div>
<div class="certificate-field">
<span class="cert-label"><?= t('track.copyright.cert_created') ?>:</span>
<span class="cert-value"><?= date('F j, Y \a\t g:i A T', strtotime($certificate['created_at'])) ?></span>
</div>
<?php if ($certificate['copyright_hash']): ?>
<div class="certificate-field">
<span class="cert-label"><?= t('track.copyright.cert_proof') ?>:</span>
<span class="cert-value" style="font-family: 'Courier New', monospace; font-size: 0.9rem; word-break: break-all;"><?= htmlspecialchars($certificate['copyright_hash']) ?></span>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<!-- SoundStudioPro Seal (Clickable for DMCA Report) -->
<div class="copyright-seal-container">
<div class="ssp-copyright-seal" onclick="openDMCAReportModal(<?= $track['id'] ?>)" style="cursor: pointer;" title="<?= t('track.copyright.click_to_report') ?>">
<div class="seal-icon">
<i class="fas fa-music"></i>
</div>
<div class="seal-content">
<div class="seal-brand">SoundStudioPro</div>
<div class="seal-text"><?= t('track.copyright.protected') ?></div>
</div>
<div class="seal-report-hint">
<i class="fas fa-exclamation-triangle"></i>
<span><?= t('track.copyright.report_infringement') ?></span>
</div>
</div>
</div>
</div>
</section>
<!-- Track Info -->
<section class="track-info fade-in" id="trackMetadataSection">
<?php
// Check if user is track creator
$isTrackOwner = $user_id && $track['user_id'] == $user_id;
?>
<div class="info-card bpm-card">
<div class="info-label"><?= t('track.info.bpm') ?></div>
<div class="info-value-wrapper">
<div class="info-value" id="bpmValue"><?php
if ($bpm) {
echo $bpm;
} else {
echo '<span style="color: #888;">β</span>';
}
?></div>
</div>
</div>
<div class="info-card key-card">
<div class="info-label"><?= t('track.info.key') ?></div>
<div class="info-value-wrapper">
<div class="info-value" id="keyValue"><?php
if ($key) {
echo htmlspecialchars($key);
if ($camelot) {
echo ' <span class="camelot-key" id="camelotValue">' . htmlspecialchars($camelot) . '</span>';
}
} else {
echo '<span style="color: #888;">β</span>';
}
?></div>
</div>
</div>
<?php if ($time_signature): ?>
<div class="info-card">
<div class="info-label"><?= t('track.info.time_signature') ?></div>
<div class="info-value"><?= $time_signature ?></div>
</div>
<?php endif; ?>
<?php if ($genre): ?>
<div class="info-card">
<div class="info-label"><?= t('track.info.genre') ?></div>
<div class="info-value"><?= htmlspecialchars($genre) ?></div>
</div>
<?php endif; ?>
<?php if ($mood): ?>
<div class="info-card">
<div class="info-label"><?= t('track.info.mood') ?></div>
<div class="info-value"><?= htmlspecialchars($mood) ?></div>
</div>
<?php endif; ?>
<div class="info-card">
<div class="info-label"><?= t('track.info.energy') ?></div>
<div class="info-value" data-analyze="energy" id="energyValue"><?php
if ($energy) {
echo htmlspecialchars($energy);
} else {
echo '<span style="color: #888;">β</span>';
}
?></div>
</div>
</section>
<!-- Enhanced Metadata Section -->
<?php
$audio_quality = json_decode($track['audio_quality'] ?? '{}', true) ?: [];
$generation_parameters = json_decode($track['generation_parameters'] ?? '{}', true) ?: [];
$processing_info = json_decode($track['processing_info'] ?? '{}', true) ?: [];
$cost_info = json_decode($track['cost_info'] ?? '{}', true) ?: [];
?>
<!-- Quality & Cost Info -->
<?php if (!empty($audio_quality) || !empty($cost_info)): ?>
<section class="enhanced-metadata fade-in">
<h2 class="section-title">
<i class="fas fa-chart-line"></i>
<?= t('track.section.quality') ?>
</h2>
<?php if (!empty($audio_quality)): ?>
<div class="metadata-grid">
<?php if (isset($audio_quality['overall_score'])): ?>
<div class="metadata-card quality-card">
<div class="metadata-label"><?= t('track.quality.overall') ?></div>
<div class="metadata-value">
<div class="quality-score">
<span class="score-number"><?= number_format($audio_quality['overall_score'], 1) ?></span>
<div class="score-bar">
<div class="score-fill" style="width: <?= ($audio_quality['overall_score'] / 10) * 100 ?>%"></div>
</div>
</div>
</div>
</div>
<?php endif; ?>
<?php if (isset($audio_quality['clarity'])): ?>
<div class="metadata-card">
<div class="metadata-label"><?= t('track.quality.clarity') ?></div>
<div class="metadata-value"><?= number_format($audio_quality['clarity'], 1) ?>/10</div>
</div>
<?php endif; ?>
<?php if (isset($audio_quality['depth'])): ?>
<div class="metadata-card">
<div class="metadata-label"><?= t('track.quality.depth') ?></div>
<div class="metadata-value"><?= number_format($audio_quality['depth'], 1) ?>/10</div>
</div>
<?php endif; ?>
<?php if (isset($audio_quality['balance'])): ?>
<div class="metadata-card">
<div class="metadata-label"><?= t('track.quality.balance') ?></div>
<div class="metadata-value"><?= number_format($audio_quality['balance'], 1) ?>/10</div>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (!empty($cost_info)): ?>
<div class="metadata-grid">
<?php if (isset($cost_info['credits_used'])): ?>
<div class="metadata-card cost-card">
<div class="metadata-label"><?= t('track.cost.credits_used') ?></div>
<div class="metadata-value"><?= $cost_info['credits_used'] ?></div>
</div>
<?php endif; ?>
<?php if (isset($cost_info['estimated_cost'])): ?>
<div class="metadata-card cost-card">
<div class="metadata-label"><?= t('track.cost.estimated') ?></div>
<div class="metadata-value">$<?= number_format($cost_info['estimated_cost'], 2) ?></div>
</div>
<?php endif; ?>
<?php if (isset($cost_info['processing_time'])): ?>
<div class="metadata-card cost-card">
<div class="metadata-label"><?= t('track.cost.processing_time') ?></div>
<div class="metadata-value"><?= $cost_info['processing_time'] ?>s</div>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</section>
<?php endif; ?>
<!-- Processing Parameters -->
<?php if (!empty($generation_parameters) || !empty($processing_info)): ?>
<section class="processing-info fade-in">
<h2 class="section-title">
<i class="fas fa-cogs"></i>
<?= t('track.section.generation') ?>
</h2>
<?php if (!empty($generation_parameters)): ?>
<div class="metadata-grid">
<?php if (isset($generation_parameters['model'])): ?>
<div class="metadata-card">
<div class="metadata-label"><?= t('track.generation.model') ?></div>
<div class="metadata-value"><?= htmlspecialchars($generation_parameters['model']) ?></div>
</div>
<?php endif; ?>
<?php if (isset($generation_parameters['version'])): ?>
<div class="metadata-card">
<div class="metadata-label"><?= t('track.generation.version') ?></div>
<div class="metadata-value"><?= htmlspecialchars($generation_parameters['version']) ?></div>
</div>
<?php endif; ?>
<?php if (isset($generation_parameters['temperature'])): ?>
<div class="metadata-card">
<div class="metadata-label"><?= t('track.generation.creativity') ?></div>
<div class="metadata-value"><?= number_format($generation_parameters['temperature'], 2) ?></div>
</div>
<?php endif; ?>
<?php if (isset($generation_parameters['top_p'])): ?>
<div class="metadata-card">
<div class="metadata-label"><?= t('track.generation.focus') ?></div>
<div class="metadata-value"><?= number_format($generation_parameters['top_p'], 2) ?></div>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php if (!empty($processing_info)): ?>
<div class="metadata-grid">
<?php if (isset($processing_info['started_at'])): ?>
<div class="metadata-card">
<div class="metadata-label"><?= t('track.processing.started') ?></div>
<div class="metadata-value"><?= date('M j, Y H:i', strtotime($processing_info['started_at'])) ?></div>
</div>
<?php endif; ?>
<?php if (isset($processing_info['completed_at'])): ?>
<div class="metadata-card">
<div class="metadata-label"><?= t('track.processing.completed') ?></div>
<div class="metadata-value"><?= date('M j, Y H:i', strtotime($processing_info['completed_at'])) ?></div>
</div>
<?php endif; ?>
<?php if (isset($processing_info['status'])): ?>
<div class="metadata-card">
<div class="metadata-label"><?= t('track.processing.status') ?></div>
<div class="metadata-value status-<?= strtolower($processing_info['status']) ?>"><?= htmlspecialchars($processing_info['status']) ?></div>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</section>
<?php endif; ?>
<!-- Lyrics Section -->
<?php if (!empty($track['lyrics'])): ?>
<section class="lyrics-section fade-in">
<h2 class="section-title">
<i class="fas fa-music"></i>
<?= t('track.lyrics.title') ?>
</h2>
<div class="lyrics-container">
<div class="lyrics-content" id="lyricsContent">
<div class="lyrics-text">
<?= nl2br(htmlspecialchars($track['lyrics'])) ?>
</div>
</div>
<button class="lyrics-toggle-btn" onclick="toggleLyricsDisplay()">
<i class="fas fa-expand-alt"></i>
<span><?= t('track.lyrics.expand') ?></span>
</button>
</div>
</section>
<?php else: ?>
<!-- Lyrics section -->
<section class="lyrics-section fade-in">
<h2 class="section-title">
<i class="fas fa-music"></i>
<?= t('track.lyrics.title') ?>
</h2>
<div style="text-align: center; color: #a0aec0; padding: 2rem;">
<p><?= t('track.lyrics.none') ?></p>
</div>
</section>
<?php endif; ?>
<!-- Variations Section -->
<?php if (!empty($trackVariations) && count($trackVariations) > 0): ?>
<section class="variations-section fade-in">
<h2 class="section-title">
<i class="fas fa-layer-group"></i>
<?= t('track.variations.title', 'Versions') ?> (<?= count($trackVariations) ?>)
</h2>
<div class="variations-container" id="variations-<?= $track['id'] ?>">
<div class="variations-grid">
<?php foreach ($trackVariations as $var):
// Use main track title with variation number for consistency
$variationDisplayTitle = ($display_title ?? $track['title']) . ' - ' . (t('track.variations.variation', 'Version') ?? 'Version') . ' ' . ($var['variation_index'] + 1);
?>
<div class="variation-item">
<div class="variation-info">
<span class="variation-title"><?= htmlspecialchars($variationDisplayTitle) ?></span>
<span class="variation-duration"><?= $var['duration'] ? gmdate('i:s', $var['duration']) : '0:00' ?></span>
</div>
<button class="variation-play-btn"
data-track-id="<?= $track['id'] ?>"
data-audio-url="<?= htmlspecialchars(getSignedAudioUrl($track['id'], $var['variation_index'])) ?>"
data-title="<?= htmlspecialchars($variationDisplayTitle) ?>"
data-artist="<?= htmlspecialchars($track['artist_name'] ?? '', ENT_QUOTES) ?>"
onclick="playVariation('<?= htmlspecialchars(getSignedAudioUrl($track['id'], $var['variation_index']), ENT_QUOTES) ?>', '<?= htmlspecialchars($variationDisplayTitle, ENT_QUOTES) ?>', '<?= htmlspecialchars($track['artist_name'] ?? 'Unknown Artist', ENT_QUOTES) ?>', <?= $track['id'] ?>)">
<i class="fas fa-play"></i>
</button>
</div>
<?php endforeach; ?>
</div>
</div>
</section>
<?php endif; ?>
<!-- Comments Section -->
<section class="comments-section fade-in">
<h2 class="section-title"><?= t('track.comments.title') ?></h2>
<?php if ($user_id): ?>
<div class="comment-form">
<textarea class="comment-input" id="commentInput" placeholder="<?= htmlspecialchars(t('track.comments.placeholder')) ?>"></textarea>
<button class="action-btn primary-btn" onclick="addComment(<?= $track['id'] ?>)">
<i class="fas fa-paper-plane"></i>
<?= t('track.comments.submit') ?>
</button>
</div>
<?php endif; ?>
<div class="comments-list" id="commentsList">
<!-- Comments will be loaded here -->
</div>
</section>
<!-- Related Tracks -->
<section class="related-tracks fade-in">
<h2 class="section-title"><?= t('track.more.title') ?></h2>
<!-- More from this artist -->
<div class="same-artist-section" style="margin-bottom: 2rem;">
<h3 class="subsection-title" style="font-size: 1.2rem; color: #888; margin-bottom: 1rem;"><?= t('track.more.from_artist', ['artist' => htmlspecialchars($track['artist_name'])]) ?></h3>
<div class="tracks-grid">
<?php
// Get more tracks from the same artist
$same_artist_query = "
SELECT mt.*, u.name as artist_name
FROM music_tracks mt
JOIN users u ON mt.user_id = u.id
WHERE mt.user_id = ? AND mt.id != ? AND mt.status = 'complete' AND mt.is_public = 1
ORDER BY mt.created_at DESC
LIMIT 4
";
$same_artist_stmt = $pdo->prepare($same_artist_query);
$same_artist_stmt->execute([$track['user_id'], $track['id']]);
$same_artist_tracks = $same_artist_stmt->fetchAll();
if (count($same_artist_tracks) > 0):
foreach ($same_artist_tracks as $related_track):
// Normalize image URL for related track
$related_track['image_url'] = normalizeTrackImageUrl($related_track);
?>
<div class="mini-track-card" onclick="window.location.href='/track.php?id=<?= $related_track['id'] ?>'">
<div class="track-cover-mini">
<?php if (!empty($related_track['image_url'])): ?>
<img src="<?= htmlspecialchars($related_track['image_url']) ?>" alt="<?= htmlspecialchars($related_track['title']) ?>">
<?php else: ?>
<img src="/uploads/track_covers/track_45_1754191923.jpg" alt="<?= htmlspecialchars(t('track.default_cover_alt'), ENT_QUOTES, 'UTF-8') ?>">
<?php endif; ?>
</div>
<div class="mini-track-title"><?= htmlspecialchars($related_track['title']) ?></div>
<div class="mini-track-artist"><?= t('track.by_prefix') ?> <a href="/artist_profile.php?id=<?= $related_track['user_id'] ?>" class="artist-link"><?= htmlspecialchars($related_track['artist_name']) ?></a></div>
</div>
<?php
endforeach;
else:
?>
<div class="no-tracks-message" style="text-align: center; padding: 2rem; color: #888; grid-column: 1 / -1;">
<p><?= t('track.more.none_artist') ?></p>
</div>
<?php endif; ?>
</div>
</div>
<!-- More from other artists -->
<div class="other-artists-section" style="margin-bottom: 2rem;">
<h3 class="subsection-title" style="font-size: 1.2rem; color: #888; margin-bottom: 1rem;"><?= t('track.more.other_artists') ?></h3>
<div class="tracks-grid">
<?php
// Get tracks from other artists
$other_artists_query = "
SELECT mt.*, u.name as artist_name
FROM music_tracks mt
JOIN users u ON mt.user_id = u.id
WHERE mt.user_id != ? AND mt.id != ? AND mt.status = 'complete' AND mt.is_public = 1
ORDER BY RAND()
LIMIT 6
";
$other_artists_stmt = $pdo->prepare($other_artists_query);
$other_artists_stmt->execute([$track['user_id'], $track['id']]);
$other_artists_tracks = $other_artists_stmt->fetchAll();
if (count($other_artists_tracks) > 0):
foreach ($other_artists_tracks as $other_track):
// Normalize image URL for related track
$other_track['image_url'] = normalizeTrackImageUrl($other_track);
?>
<div class="mini-track-card" onclick="window.location.href='/track.php?id=<?= $other_track['id'] ?>'">
<div class="track-cover-mini">
<?php if (!empty($other_track['image_url'])): ?>
<img src="<?= htmlspecialchars($other_track['image_url']) ?>" alt="<?= htmlspecialchars($other_track['title']) ?>">
<?php else: ?>
<img src="/uploads/track_covers/track_45_1754191923.jpg" alt="<?= htmlspecialchars(t('track.default_cover_alt'), ENT_QUOTES, 'UTF-8') ?>">
<?php endif; ?>
</div>
<div class="mini-track-title"><?= htmlspecialchars($other_track['title']) ?></div>
<div class="mini-track-artist"><?= t('track.by_prefix') ?> <a href="/artist_profile.php?id=<?= $other_track['user_id'] ?>" class="artist-link"><?= htmlspecialchars($other_track['artist_name']) ?></a></div>
</div>
<?php
endforeach;
else:
?>
<div class="no-tracks-message" style="text-align: center; padding: 2rem; color: #888; grid-column: 1 / -1;">
<p><?= t('track.more.none_other_artist') ?></p>
<p><?= t('track.more.check_back') ?></p>
</div>
<?php endif; ?>
</div>
</div>
<!-- More from other genres -->
<div class="other-genres-section" style="margin-bottom: 4rem;">
<h3 class="subsection-title" style="font-size: 1.2rem; color: #888; margin-bottom: 1rem;"><?= t('track.more.other_genres') ?></h3>
<div class="genres-grid">
<?php
// Get actual genres from database metadata (only from public tracks)
$genres_query = "
SELECT DISTINCT
CASE
WHEN JSON_EXTRACT(metadata, '$.genre') IS NOT NULL AND JSON_EXTRACT(metadata, '$.genre') != ''
THEN JSON_EXTRACT(metadata, '$.genre')
WHEN JSON_EXTRACT(metadata, '$.style') IS NOT NULL AND JSON_EXTRACT(metadata, '$.style') != ''
THEN JSON_EXTRACT(metadata, '$.style')
ELSE NULL
END as genre
FROM music_tracks
WHERE metadata IS NOT NULL
AND metadata != ''
AND status = 'complete'
AND is_public = 1
AND (
JSON_EXTRACT(metadata, '$.genre') IS NOT NULL OR
JSON_EXTRACT(metadata, '$.style') IS NOT NULL
)
LIMIT 12
";
$genres_stmt = $pdo->prepare($genres_query);
$genres_stmt->execute();
$available_genres = $genres_stmt->fetchAll(PDO::FETCH_COLUMN);
if (count($available_genres) > 0):
foreach ($available_genres as $genre):
if (!empty($genre)):
?>
<button class="genre-button" onclick="window.location.href='/community_fixed.php?genre=<?= urlencode($genre) ?>'">
<?= htmlspecialchars($genre) ?>
</button>
<?php
endif;
endforeach;
else:
?>
<div class="no-genres-message" style="text-align: center; padding: 2rem; color: #888; grid-column: 1 / -1;">
<p>No genre data available in track metadata.</p>
</div>
<?php endif; ?>
</div>
</div>
</section>
</div>
<!-- Audio Element -->
<?php $signedAudioUrl = getSignedAudioUrl($track['id']); ?>
<audio id="audioPlayer" preload="metadata">
<source src="<?= htmlspecialchars($signedAudioUrl) ?>" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
<script>
// Global variables
let isPlaying = false;
let currentTrackId = <?= $track['id'] ?>;
let audioPlayer = document.getElementById('audioPlayer');
let playBtn = document.getElementById('playBtn');
let playIcon = document.getElementById('playIcon');
let progressBar = document.getElementById('progressBar');
let currentTimeSpan = document.getElementById('currentTime');
let totalTimeSpan = document.getElementById('totalTime');
// Volume slider removed - using global player volume control instead
// Setup add to cart button event listener
document.addEventListener('DOMContentLoaded', function() {
const addToCartBtn = document.getElementById('addToCartBtn');
if (addToCartBtn) {
addToCartBtn.addEventListener('click', function(e) {
e.preventDefault();
const trackId = parseInt(this.getAttribute('data-track-id'));
const title = this.getAttribute('data-track-title') || '';
const price = parseFloat(this.getAttribute('data-track-price')) || 1.99;
console.log('π Button clicked, calling addToCart:', { trackId, title, price });
addToCart(trackId, title, price, this);
});
console.log('π Add to cart button event listener attached');
} else {
console.error('π Add to cart button not found!');
}
});
// Enhanced track data for audio analysis
window.trackDuration = <?= $track['duration'] ?? 180 ?>;
// Initialize page - function approach for AJAX compatibility
function initializeTrackPage() {
console.log('π΅ DEBUG: initializeTrackPage called');
console.log('π΅ DEBUG: Current URL:', window.location.href);
console.log('π΅ DEBUG: Track ID from PHP:', <?= $track['id'] ?>);
// Extract track ID from URL for AJAX navigation
const urlParams = new URLSearchParams(window.location.search);
const newTrackId = urlParams.get('id');
// Update current track ID if it changed (AJAX navigation)
if (newTrackId && newTrackId !== currentTrackId.toString()) {
console.log('π΅ Track ID changed via AJAX:', currentTrackId, 'β', newTrackId);
currentTrackId = parseInt(newTrackId);
}
console.log('π΅ Initializing track page:', currentTrackId);
// Clear any existing timers or listeners to prevent duplicates
if (window.trackPageInitialized) {
console.log('π΅ Track page already initialized, cleaning up...');
// Clear any intervals or timeouts that might exist
if (window.trackUpdateInterval) {
clearInterval(window.trackUpdateInterval);
}
}
// Re-get DOM elements (they might have changed during AJAX navigation)
audioPlayer = document.getElementById('audioPlayer');
playBtn = document.getElementById('playBtn');
playIcon = document.getElementById('playIcon');
progressBar = document.getElementById('progressBar');
currentTimeSpan = document.getElementById('currentTime');
totalTimeSpan = document.getElementById('totalTime');
console.log('π΅ DEBUG: Audio player element:', audioPlayer);
console.log('π΅ DEBUG: Play button element:', playBtn);
initializeAudioPlayer();
// Only load comments if track ID actually changed
if (!window.lastTrackId || window.lastTrackId !== currentTrackId) {
console.log('π΅ Loading new track data for ID:', currentTrackId);
// Update track content if this is an AJAX load
updateTrackContent(currentTrackId);
loadComments(currentTrackId);
window.lastTrackId = currentTrackId;
} else {
console.log('π΅ Track ID unchanged, skipping data reload');
}
// Mark as initialized
window.trackPageInitialized = true;
console.log('π΅ DEBUG: Track page initialization complete');
}
// Initialize on DOM ready only - no AJAX conflicts
document.addEventListener('DOMContentLoaded', initializeTrackPage);
// Fallback initialization to ensure page loads completely
window.addEventListener('load', function() {
if (!window.trackPageInitialized) {
console.log('π΅ Fallback initialization - page not fully loaded');
setTimeout(initializeTrackPage, 100);
}
});
// Audio Player Functions
function initializeAudioPlayer() {
if (!audioPlayer) {
console.error('π΅ Audio player element not found');
return;
}
// Remove existing event listeners to prevent duplicates
audioPlayer.removeEventListener('loadedmetadata', handleLoadedMetadata);
audioPlayer.removeEventListener('timeupdate', handleTimeUpdate);
audioPlayer.removeEventListener('ended', handleAudioEnded);
// Add new event listeners
audioPlayer.addEventListener('loadedmetadata', handleLoadedMetadata);
audioPlayer.addEventListener('timeupdate', handleTimeUpdate);
audioPlayer.addEventListener('ended', handleAudioEnded);
}
// Event handler functions (defined separately for proper removal)
function handleLoadedMetadata() {
if (totalTimeSpan) {
totalTimeSpan.textContent = formatTime(audioPlayer.duration);
}
}
function handleTimeUpdate() {
if (audioPlayer && progressBar && currentTimeSpan) {
const currentTime = audioPlayer.currentTime;
const duration = audioPlayer.duration;
if (duration && duration > 0 && !isNaN(currentTime) && !isNaN(duration)) {
const progress = (currentTime / duration) * 100;
progressBar.style.width = Math.min(100, Math.max(0, progress)) + '%';
currentTimeSpan.textContent = formatTime(currentTime);
}
}
}
function handleAudioEnded() {
isPlaying = false;
if (playIcon) playIcon.className = 'fas fa-play';
if (playBtn) playBtn.classList.remove('playing');
recordPlay();
}
// Volume slider removed - using global player volume control instead
function togglePlay() {
console.log('π΅ togglePlay called');
console.log('π΅ Global player available:', !!window.enhancedGlobalPlayer);
console.log('π΅ playTrack function:', typeof window.enhancedGlobalPlayer?.playTrack);
// Use global player instead of local audio player
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
const trackId = <?= $track['id'] ?>;
const audioUrl = <?= json_encode($signedAudioUrl) ?>;
const title = <?= json_encode($track['title']) ?>;
const artist = <?= json_encode($track['artist_name'] ?? 'Unknown Artist') ?>;
const duration = <?= $playbackDuration ?? 180 ?>;
console.log('π΅ Track data:', { trackId, audioUrl, title, artist });
if (isPlaying) {
// Currently playing - pause the global player
console.log('π΅ Pausing global player');
window.enhancedGlobalPlayer.togglePlayPause();
// Update local UI to show paused state
isPlaying = false;
playIcon.className = 'fas fa-play';
playBtn.classList.remove('playing');
} else {
// Currently paused - play the track
console.log('π΅ Playing track through global player:', { trackId, title, artist, audioUrl });
// Ensure audio URL is absolute if it's relative
let finalAudioUrl = audioUrl;
if (audioUrl && !audioUrl.startsWith('http') && !audioUrl.startsWith('//')) {
// Make relative URL absolute
if (audioUrl.startsWith('/')) {
finalAudioUrl = window.location.origin + audioUrl;
} else {
finalAudioUrl = window.location.origin + '/' + audioUrl;
}
console.log('π΅ Converted relative URL to absolute:', finalAudioUrl);
}
// Verify audio URL is not empty
if (!finalAudioUrl || finalAudioUrl.trim() === '') {
console.error('β Audio URL is empty!');
showNotification('β Audio file not available. Please contact support.', 'error');
return;
}
// Call global player's playTrack function
// Signature: playTrack(audioUrl, title, artist, trackId, artistId, skipIndexSearch)
const success = window.enhancedGlobalPlayer.playTrack(finalAudioUrl, title, artist, trackId);
if (success) {
// Update local UI to show playing state
isPlaying = true;
playIcon.className = 'fas fa-pause';
playBtn.classList.add('playing');
recordPlay();
// Show now playing notification
if (typeof showNotification === 'function') {
showNotification(`Now playing: ${title}`, 'info');
}
// Also load audio into local player for time updates
if (audioPlayer) {
audioPlayer.src = finalAudioUrl;
audioPlayer.load();
// Don't play it, just load it so we can track time
// The global player will handle actual playback
}
} else {
console.error('β Global player failed to play track');
showNotification('β Failed to play track. Please try again or contact support if the issue persists.', 'error');
}
}
} else {
console.error('β Global player not available, falling back to local player');
// Fallback to local player if global player is not available
if (isPlaying) {
audioPlayer.pause();
isPlaying = false;
playIcon.className = 'fas fa-play';
playBtn.classList.remove('playing');
} else {
audioPlayer.play();
isPlaying = true;
playIcon.className = 'fas fa-pause';
playBtn.classList.add('playing');
recordPlay();
// Show now playing notification
const title = <?= json_encode($track['title']) ?>;
if (typeof showNotification === 'function') {
showNotification(`Now playing: ${title}`, 'info');
}
}
}
}
function seekTo(event) {
const rect = event.currentTarget.getBoundingClientRect();
const clickX = event.clientX - rect.left;
const width = rect.width;
const seekTime = (clickX / width) * audioPlayer.duration;
audioPlayer.currentTime = seekTime;
}
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
function recordPlay() {
fetch('/api/record_play.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
track_id: currentTrackId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
updatePlayCount();
}
})
.catch(error => console.error('Error recording play:', error));
}
function updatePlayCount() {
const playCountElement = document.querySelector('.stat-item:first-child .stat-value');
if (playCountElement) {
const currentCount = parseInt(playCountElement.textContent.replace(/,/g, '')) || 0;
const newCount = currentCount + 1;
playCountElement.textContent = newCount.toLocaleString();
}
}
// Sync local UI with global player state
function syncWithGlobalPlayer() {
let timeUpdated = false;
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.isPlaying === 'function') {
const globalIsPlaying = window.enhancedGlobalPlayer.isPlaying();
if (globalIsPlaying !== isPlaying) {
isPlaying = globalIsPlaying;
if (globalIsPlaying) {
playIcon.className = 'fas fa-pause';
playBtn.classList.add('playing');
} else {
playIcon.className = 'fas fa-play';
playBtn.classList.remove('playing');
}
}
// Update time display from global player
if (globalIsPlaying) {
let currentTime = null;
let duration = null;
// Try multiple ways to get current time
// Method 1: getCurrentTime() method
if (window.enhancedGlobalPlayer.getCurrentTime && typeof window.enhancedGlobalPlayer.getCurrentTime === 'function') {
try {
currentTime = window.enhancedGlobalPlayer.getCurrentTime();
} catch(e) {
console.warn('getCurrentTime() failed:', e);
}
}
// Method 2: Direct audio element access
if (currentTime === null || currentTime === undefined) {
if (window.enhancedGlobalPlayer.audio && window.enhancedGlobalPlayer.audio.currentTime !== undefined) {
currentTime = window.enhancedGlobalPlayer.audio.currentTime;
}
}
// Method 3: Try _audio or audioElement properties
if ((currentTime === null || currentTime === undefined) && window.enhancedGlobalPlayer._audio) {
currentTime = window.enhancedGlobalPlayer._audio.currentTime;
}
if ((currentTime === null || currentTime === undefined) && window.enhancedGlobalPlayer.audioElement) {
currentTime = window.enhancedGlobalPlayer.audioElement.currentTime;
}
// Try to get duration
if (window.enhancedGlobalPlayer.getDuration && typeof window.enhancedGlobalPlayer.getDuration === 'function') {
try {
duration = window.enhancedGlobalPlayer.getDuration();
} catch(e) {
console.warn('getDuration() failed:', e);
}
}
if (!duration || duration === 0) {
if (window.enhancedGlobalPlayer.audio && window.enhancedGlobalPlayer.audio.duration) {
duration = window.enhancedGlobalPlayer.audio.duration;
} else if (window.enhancedGlobalPlayer._audio && window.enhancedGlobalPlayer._audio.duration) {
duration = window.enhancedGlobalPlayer._audio.duration;
} else if (window.enhancedGlobalPlayer.audioElement && window.enhancedGlobalPlayer.audioElement.duration) {
duration = window.enhancedGlobalPlayer.audioElement.duration;
} else {
duration = <?= $track['duration'] ?? 180 ?>;
}
}
// Update UI if we have valid time data
if (currentTime !== null && currentTime !== undefined && !isNaN(currentTime) && currentTimeSpan && progressBar) {
currentTimeSpan.textContent = formatTime(currentTime);
if (duration && duration > 0 && !isNaN(duration)) {
const progress = (currentTime / duration) * 100;
progressBar.style.width = Math.min(100, Math.max(0, progress)) + '%';
}
timeUpdated = true;
}
}
}
// Also update from local audio player if it exists (even if global player is playing)
// This ensures time updates work even if global player doesn't expose time methods
if (audioPlayer && audioPlayer.src && audioPlayer.currentTime !== undefined && !isNaN(audioPlayer.currentTime)) {
// Sync local player with global player if possible
if (window.enhancedGlobalPlayer && isPlaying) {
// Try to sync currentTime from global player to local player
let globalTime = null;
if (window.enhancedGlobalPlayer.getCurrentTime && typeof window.enhancedGlobalPlayer.getCurrentTime === 'function') {
try {
globalTime = window.enhancedGlobalPlayer.getCurrentTime();
} catch(e) {}
}
if (globalTime === null && window.enhancedGlobalPlayer.audio && window.enhancedGlobalPlayer.audio.currentTime !== undefined) {
globalTime = window.enhancedGlobalPlayer.audio.currentTime;
}
if (globalTime !== null && !isNaN(globalTime) && Math.abs(audioPlayer.currentTime - globalTime) > 0.5) {
audioPlayer.currentTime = globalTime;
}
}
// Update UI from local player
if (currentTimeSpan && progressBar && audioPlayer.duration && !isNaN(audioPlayer.duration) && audioPlayer.duration > 0) {
currentTimeSpan.textContent = formatTime(audioPlayer.currentTime);
const progress = (audioPlayer.currentTime / audioPlayer.duration) * 100;
progressBar.style.width = Math.min(100, Math.max(0, progress)) + '%';
timeUpdated = true;
}
}
// Debug: Log if time is not updating (only once per 5 seconds to avoid spam)
if (!timeUpdated && isPlaying && (!window.lastTimeDebug || Date.now() - window.lastTimeDebug > 5000)) {
window.lastTimeDebug = Date.now();
console.log('β±οΈ Time not updating. Debug info:', {
hasGetCurrentTime: !!(window.enhancedGlobalPlayer && window.enhancedGlobalPlayer.getCurrentTime),
hasAudio: !!(window.enhancedGlobalPlayer && window.enhancedGlobalPlayer.audio),
hasLocalAudio: !!audioPlayer,
localAudioSrc: audioPlayer ? audioPlayer.src : 'none',
localAudioCurrentTime: audioPlayer ? audioPlayer.currentTime : 'none',
isPlaying: isPlaying
});
}
}
// Check global player state periodically and update time
setInterval(syncWithGlobalPlayer, 100); // Update more frequently for smooth time display
// Listen for global player track changes
function checkGlobalPlayerTrack() {
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.getCurrentTrack === 'function') {
const globalTrack = window.enhancedGlobalPlayer.getCurrentTrack();
const currentTrackId = <?= $track['id'] ?>;
if (globalTrack && globalTrack.id !== currentTrackId) {
// Global player is playing a different track, update local UI
isPlaying = false;
playIcon.className = 'fas fa-play';
playBtn.classList.remove('playing');
}
}
}
// Check for track changes every 2 seconds
setInterval(checkGlobalPlayerTrack, 2000);
// Notification function
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${type === 'success' ? 'linear-gradient(135deg, #48bb78 0%, #38a169 100%)' :
type === 'error' ? 'linear-gradient(135deg, #e53e3e 0%, #c53030 100%)' :
'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'};
color: white;
padding: 15px 20px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
z-index: 10000;
font-size: 14px;
font-weight: 500;
animation: slideIn 0.3s ease-out;
max-width: 300px;
word-wrap: break-word;
`;
notification.textContent = message;
document.body.appendChild(notification);
// Remove after 3 seconds
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease-in';
setTimeout(() => document.body.removeChild(notification), 300);
}, 3000);
}
// Like Function
function toggleLike(trackId, button) {
// For AJAX navigation, get current track ID from URL
const currentTrackId = new URLSearchParams(window.location.search).get('id') || trackId;
console.log('π΅ Toggling like for track:', currentTrackId);
fetch('/api/toggle_like.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
track_id: currentTrackId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (data.liked) {
button.classList.add('liked');
button.innerHTML = '<i class="fas fa-heart"></i> Liked';
} else {
button.classList.remove('liked');
button.innerHTML = '<i class="fas fa-heart"></i> Like';
}
updateLikeCount(data.like_count);
}
})
.catch(error => console.error('Error toggling like:', error));
}
function updateLikeCount(count) {
const likeCountElement = document.querySelector('.stat-item:nth-child(2) .stat-value');
if (likeCountElement) {
likeCountElement.textContent = count.toLocaleString();
}
}
// Comment Functions
function addComment(trackId) {
// For AJAX navigation, get current track ID from URL
const currentTrackId = new URLSearchParams(window.location.search).get('id') || trackId;
const commentInput = document.getElementById('commentInput');
const comment = commentInput.value.trim();
if (!comment) return;
console.log('π΅ Adding comment for track:', currentTrackId);
fetch('/api/add_comment.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
track_id: currentTrackId,
comment: comment
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
commentInput.value = '';
loadComments(trackId);
}
})
.catch(error => console.error('Error adding comment:', error));
}
function loadComments(trackId) {
// For AJAX navigation, get current track ID from URL
const currentTrackId = new URLSearchParams(window.location.search).get('id') || trackId;
console.log('π΅ Loading comments for track:', currentTrackId);
fetch(`/api/get_comments.php?track_id=${currentTrackId}`)
.then(response => response.json())
.then(data => {
const commentsList = document.getElementById('commentsList');
commentsList.innerHTML = '';
data.comments.forEach(comment => {
const commentElement = document.createElement('div');
commentElement.className = 'comment fade-in';
const authorLink = comment.author_id
? `<a href="/artist_profile.php?id=${comment.author_id}" class="comment-author-link">${comment.author_name}</a>`
: `<span class="comment-author">${comment.author_name}</span>`;
commentElement.innerHTML = `
<div class="comment-header">
${authorLink}
<span class="comment-date">${formatDate(comment.created_at)}</span>
</div>
<div class="comment-text">${comment.comment}</div>
`;
commentsList.appendChild(commentElement);
});
})
.catch(error => console.error('Error loading comments:', error));
}
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString();
}
// Update track content for AJAX navigation
// Function to update BPM and Key display
function updateBPMAndKey(metadata) {
const bpmValue = metadata['bpm'] ?? null;
const keyValue = metadata['key'] ?? null;
const camelotValue = metadata['numerical_key'] ?? null;
// Normalize BPM (same logic as PHP)
let normalizedBpm = bpmValue;
if (normalizedBpm) {
if (normalizedBpm > 140) {
normalizedBpm = normalizedBpm / 2;
if (normalizedBpm > 140) {
normalizedBpm = normalizedBpm / 2;
}
} else if (Math.abs(normalizedBpm - 188) < 2) {
normalizedBpm = 94;
} else if (normalizedBpm < 50 && normalizedBpm > 0) {
const doubled = normalizedBpm * 2;
if (doubled <= 140) {
normalizedBpm = doubled;
}
}
if (normalizedBpm > 140) normalizedBpm = 140;
if (normalizedBpm < 40) normalizedBpm = 60;
}
// Update BPM display
const bpmElement = document.getElementById('bpmValue');
if (bpmElement) {
if (normalizedBpm) {
bpmElement.textContent = normalizedBpm;
} else {
bpmElement.innerHTML = '<span style="color: #888;">β</span>';
}
}
// Update Key display
const keyElement = document.getElementById('keyValue');
const camelotElement = document.getElementById('camelotValue');
if (keyElement) {
if (keyValue) {
keyElement.innerHTML = keyValue;
if (camelotValue && camelotElement) {
camelotElement.textContent = camelotValue;
} else if (camelotValue) {
keyElement.innerHTML += ` <span class="camelot-key" id="camelotValue">${camelotValue}</span>`;
}
} else {
keyElement.innerHTML = '<span style="color: #888;">β</span>';
}
}
console.log('π΅ Updated BPM/Key display:', { bpm: normalizedBpm, key: keyValue, camelot: camelotValue });
}
function updateTrackContent(trackId) {
console.log('π΅ Updating track content for ID:', trackId);
// Fetch track data via AJAX
fetch(`/api/get_track_data.php?id=${trackId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
const track = data.track;
// Update track title
const titleElement = document.getElementById('trackTitle');
if (titleElement) {
titleElement.textContent = track.title || 'Untitled Track';
}
// Update artist name and link
const artistElement = document.getElementById('artistLink');
if (artistElement) {
artistElement.textContent = track.artist_name;
artistElement.href = `/artist_profile.php?id=${track.user_id}`;
}
// Prompt hidden - internal use only
// Update audio source with signed URL
const audioElement = document.getElementById('audioPlayer');
if (audioElement && track.id) {
// Get signed URL from API to prevent URL sharing
fetch('/api/get_audio_token.php?track_id=' + track.id)
.then(r => r.json())
.then(data => {
if (data.success && data.url) {
audioElement.src = data.url;
audioElement.load();
}
})
.catch(err => console.error('Failed to get audio token:', err));
}
// Update page title
document.title = `${track.title} by ${track.artist_name} - SoundStudioPro`;
// Update BPM and Key if metadata is available
if (track.metadata) {
const metadata = typeof track.metadata === 'string' ? JSON.parse(track.metadata) : track.metadata;
updateBPMAndKey(metadata);
}
console.log('π΅ Track content updated successfully');
} else {
console.error('π΅ Failed to load track data:', data.error);
}
})
.catch(error => {
console.error('π΅ Error updating track content:', error);
});
}
// Listen for analysis completion events (from batch analysis or single analysis)
window.addEventListener('message', function(event) {
if (event.data && event.data.type === 'analysis_complete') {
const { trackId, bpm, key, camelot } = event.data;
if (trackId === <?= $track_id ?>) {
console.log('π΅ Analysis complete event received, updating display');
// Fetch fresh track data to get normalized values
updateTrackContent(trackId);
}
}
});
// Also listen for storage events (if analysis saves to localStorage)
window.addEventListener('storage', function(event) {
if (event.key === 'analysis_complete_' + <?= $track_id ?>) {
console.log('π΅ Analysis complete detected via storage, updating display');
updateTrackContent(<?= $track_id ?>);
}
});
// Poll for metadata updates every 5 seconds if on track page
// This ensures BPM/key updates even if message events don't work
let metadataPollInterval = null;
let lastMetadataHash = null;
function startMetadataPolling() {
if (metadataPollInterval) return; // Already polling
metadataPollInterval = setInterval(async () => {
try {
const response = await fetch(`/api/get_track_data.php?id=<?= $track_id ?>`);
const data = await response.json();
if (data.success && data.track && data.track.metadata) {
const metadata = typeof data.track.metadata === 'string'
? JSON.parse(data.track.metadata)
: data.track.metadata;
// Create hash of BPM/key to detect changes
const currentHash = JSON.stringify({
bpm: metadata['bpm'],
key: metadata['key'],
camelot: metadata['numerical_key']
});
// If metadata changed, update display
if (currentHash !== lastMetadataHash) {
console.log('π΅ Metadata changed detected, updating display');
updateBPMAndKey(metadata);
lastMetadataHash = currentHash;
}
}
} catch (error) {
console.error('π΅ Error polling metadata:', error);
}
}, 5000); // Poll every 5 seconds
}
// Start polling when page loads
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startMetadataPolling);
} else {
startMetadataPolling();
}
// Stop polling when page unloads
window.addEventListener('beforeunload', () => {
if (metadataPollInterval) {
clearInterval(metadataPollInterval);
}
});
// Lyrics Toggle Function
function toggleLyricsDisplay() {
const lyricsContent = document.getElementById('lyricsContent');
const toggleBtn = document.querySelector('.lyrics-toggle-btn');
const toggleSpan = toggleBtn.querySelector('span');
const toggleIcon = toggleBtn.querySelector('i');
if (lyricsContent.classList.contains('expanded')) {
// Collapse
lyricsContent.classList.remove('expanded');
toggleSpan.textContent = 'Expand Lyrics';
toggleBtn.classList.remove('expanded');
toggleIcon.className = 'fas fa-expand-alt';
} else {
// Expand
lyricsContent.classList.add('expanded');
toggleSpan.textContent = 'Collapse Lyrics';
toggleBtn.classList.add('expanded');
toggleIcon.className = 'fas fa-compress-alt';
}
}
// Utility Functions
// Track if a request is already in progress to prevent double-clicks
let addToCartInProgress = false;
function addToCart(trackId, title, price, buttonElement) {
console.log('π addToCart called with:', { trackId, title, price, buttonElement });
// Check if this is a private track accessed via share link
const isPrivateViaShare = <?= $isPrivateViaShare ? 'true' : 'false' ?>;
if (isPrivateViaShare) {
console.log('π Private track accessed via share link - cannot add to cart');
if (typeof showNotification === 'function') {
showNotification('<?= addslashes(t('track.private_not_for_sale_message')) ?>', 'warning');
} else {
alert('<?= addslashes(t('track.private_not_for_sale_message')) ?>');
}
return;
}
// Prevent double-clicks - check if request already in progress
if (addToCartInProgress) {
console.log('π Request already in progress, ignoring duplicate click');
if (typeof showNotification === 'function') {
showNotification('Please wait, adding to cart...', 'info');
}
return;
}
// Check if user is logged in
const isLoggedIn = <?= $user_id ? 'true' : 'false' ?>;
if (!isLoggedIn) {
console.log('π User not logged in, redirecting...');
if (typeof showNotification === 'function') {
showNotification('Please log in to add tracks to cart', 'warning');
} else {
alert('Please log in to add tracks to cart');
}
setTimeout(() => {
window.location.href = '/auth/login.php?redirect=' + encodeURIComponent(window.location.pathname + window.location.search);
}, 2000);
return;
}
// Get the button element to show loading state
const button = buttonElement || document.getElementById('addToCartBtn') || document.querySelector('.action-btn.primary-btn');
const originalHTML = button ? button.innerHTML : '';
console.log('π Button element:', button);
// Set processing flag and disable button IMMEDIATELY to prevent double-clicks
addToCartInProgress = true;
if (button) {
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Adding...';
}
// For AJAX navigation, get current track ID from URL
const currentTrackId = new URLSearchParams(window.location.search).get('id') || trackId;
const artistPlan = '<?= strtolower($track['artist_plan'] ?? 'free') ?>';
console.log('π Sending request:', { currentTrackId, artistPlan });
// Use FormData to match cart.php expectations
const formData = new FormData();
formData.append('track_id', currentTrackId);
formData.append('action', 'add');
formData.append('artist_plan', artistPlan);
fetch('/cart.php', {
method: 'POST',
body: formData
})
.then(response => {
console.log('π Response status:', response.status, response.statusText);
console.log('π Response headers:', response.headers.get('content-type'));
// Check if response is OK
if (!response.ok) {
return response.text().then(text => {
console.error('π HTTP error response:', text);
throw new Error(`HTTP ${response.status}: ${text.substring(0, 100)}`);
});
}
// Check if response is JSON
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
return response.text().then(text => {
console.error('π Non-JSON response:', text);
throw new Error('Invalid response format: ' + text.substring(0, 100));
});
}
return response.json();
})
.then(data => {
console.log('π Response data:', data);
if (data.success) {
if (typeof showNotification === 'function') {
showNotification('β
Track added to cart!', 'success');
} else {
alert('Track added to cart!');
}
// Update cart counter if it exists
const cartCounts = document.querySelectorAll('.cart-count');
if (cartCounts.length > 0 && data.cart_count !== undefined) {
cartCounts.forEach(count => {
count.textContent = data.cart_count;
if (data.cart_count > 0) {
count.style.display = 'flex';
} else {
count.style.display = 'none';
}
});
} else if (cartCounts.length > 0) {
cartCounts.forEach(count => {
const currentCount = parseInt(count.textContent) || 0;
count.textContent = currentCount + 1;
count.style.display = 'flex';
});
}
// Refresh cart modal if it's currently open
const cartModal = document.getElementById('cartModal');
if (cartModal && cartModal.style.display === 'flex') {
if (typeof refreshCartModal === 'function') {
refreshCartModal();
}
}
} else {
const errorMsg = data.message || 'Failed to add track to cart';
console.error('π Error from server:', errorMsg);
// Special handling for "already in cart" - show as warning, not error
if (data.already_in_cart) {
if (typeof showNotification === 'function') {
showNotification('βΉοΈ ' + errorMsg, 'warning');
} else {
alert(errorMsg);
}
} else {
if (typeof showNotification === 'function') {
showNotification('β Error: ' + errorMsg, 'error');
} else {
alert('Error: ' + errorMsg);
}
}
}
})
.catch(error => {
console.error('π Fetch error:', error);
const errorMsg = error.message || 'Network error occurred';
if (typeof showNotification === 'function') {
showNotification('β Error: ' + errorMsg, 'error');
} else {
alert('Error: ' + errorMsg);
}
})
.finally(() => {
// Clear processing flag and restore button state
addToCartInProgress = false;
if (button) {
button.disabled = false;
button.innerHTML = originalHTML || '<i class="fas fa-shopping-cart"></i> <?= t("artist_profile.add_to_cart") ?>';
}
});
}
function shareTrack(trackId, title, artist) {
// For AJAX navigation, get current track data from the page
// Use PHP-rendered track ID first (works with clean URLs), then URL param, then passed trackId
const currentTrackId = <?= json_encode($track['id']) ?> || new URLSearchParams(window.location.search).get('id') || trackId;
const currentTitle = document.querySelector('#trackTitle')?.textContent || title;
const currentArtist = document.querySelector('#artistLink')?.textContent || artist;
// Use clean URL format for SEO-friendly sharing
const url = `${window.location.origin}/track/${currentTrackId}/`;
const shareText = `Check out "${currentTitle}" by ${currentArtist} on SoundStudioPro! π΅`;
const shareData = {
title: `${currentTitle} by ${currentArtist}`,
text: shareText,
url: url
};
// Record the share in the database (matching community_fixed.php)
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'share',
track_id: currentTrackId,
platform: 'web'
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('π Share recorded for track:', currentTrackId);
}
})
.catch(error => {
console.error('π Share recording error:', error);
});
// Try native share API first (mobile)
if (navigator.share) {
if (navigator.canShare && navigator.canShare(shareData)) {
navigator.share(shareData)
.then(() => {
if (typeof showNotification === 'function') {
showNotification('β
Track shared successfully!', 'success');
}
})
.catch(error => {
if (error.name !== 'AbortError') {
fallbackShare(url, shareText);
}
});
} else if (!navigator.canShare) {
navigator.share(shareData)
.then(() => {
if (typeof showNotification === 'function') {
showNotification('β
Track shared successfully!', 'success');
}
})
.catch(error => {
if (error.name !== 'AbortError') {
fallbackShare(url, shareText);
}
});
} else {
fallbackShare(url, shareText);
}
} else {
fallbackShare(url, shareText);
}
}
// Fallback sharing function (matching community_fixed.php pattern)
function fallbackShare(url, shareText) {
const shareData = `${shareText}\n${url}`;
const isSecureContext = window.isSecureContext || location.protocol === 'https:' || location.hostname === 'localhost';
if (isSecureContext && navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(shareData)
.then(() => {
if (typeof showNotification === 'function') {
showNotification('π Track link copied to clipboard!', 'success');
} else {
alert('Track link copied to clipboard!');
}
})
.catch(error => {
fallbackCopyTrack(shareData);
});
} else {
fallbackCopyTrack(shareData);
}
}
// Fallback copy using textarea (better mobile support)
function fallbackCopyTrack(shareData) {
const textarea = document.createElement('textarea');
textarea.value = shareData;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
textarea.style.top = '-9999px';
textarea.style.opacity = '0';
textarea.setAttribute('readonly', '');
document.body.appendChild(textarea);
if (/Android/i.test(navigator.userAgent)) {
textarea.style.position = 'absolute';
textarea.style.left = '0';
textarea.style.top = '0';
textarea.style.width = '1px';
textarea.style.height = '1px';
textarea.style.opacity = '0.01';
}
textarea.focus();
textarea.select();
textarea.setSelectionRange(0, shareData.length);
try {
const successful = document.execCommand('copy');
document.body.removeChild(textarea);
if (successful) {
if (typeof showNotification === 'function') {
showNotification('π Track link copied to clipboard!', 'success');
} else {
alert('Track link copied to clipboard!');
}
} else {
prompt('Copy this link:', shareData);
}
} catch (error) {
document.body.removeChild(textarea);
prompt('Copy this link:', shareData);
}
}
// Share current track (gets data from page elements)
function shareCurrentTrack() {
// Use PHP-rendered track ID (works with clean URLs like /track/123)
const currentTrackId = <?= json_encode($track['id']) ?> || new URLSearchParams(window.location.search).get('id');
const currentTitle = document.querySelector('#trackTitle')?.textContent || 'Untitled Track';
const currentArtist = document.querySelector('#artistLink')?.textContent || 'Unknown Artist';
console.log('π΅ Share current track called:', { currentTrackId, currentTitle, currentArtist });
// Call the main share function with current data
shareTrack(currentTrackId, currentTitle, currentArtist);
}
// Play variation function - uses global player
function playVariation(audioUrl, title, artist = null, trackId = null) {
console.log('π΅ Playing variation:', { audioUrl, title, artist, trackId });
// Get artist from button data if not provided
if (!artist) {
const btn = event?.target?.closest('.variation-play-btn');
if (btn) {
artist = btn.getAttribute('data-artist') || '<?= htmlspecialchars($track['artist_name'] ?? 'Unknown Artist', ENT_QUOTES) ?>';
trackId = trackId || btn.getAttribute('data-track-id') || <?= $track['id'] ?>;
}
}
// Use global player instead of local audio player
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
console.log('π΅ Using global player for variation');
const finalTrackId = trackId || <?= $track['id'] ?>;
const finalArtist = artist || '<?= htmlspecialchars($track['artist_name'] ?? 'Unknown Artist', ENT_QUOTES) ?>';
const success = window.enhancedGlobalPlayer.playTrack(audioUrl, title, finalArtist, finalTrackId);
if (success) {
// Update local UI to show playing state
isPlaying = true;
if (playIcon) playIcon.className = 'fas fa-pause';
if (playBtn) playBtn.classList.add('playing');
// Record play
if (typeof recordPlay === 'function') {
recordPlay();
}
if (typeof showNotification === 'function') {
showNotification(`Now playing: ${title}`, 'info');
}
} else {
console.error('π΅ Failed to play variation through global player');
if (typeof showNotification === 'function') {
showNotification('Failed to play variation.', 'error');
}
}
} else {
console.warn('π΅ Global player not available, falling back to local player');
// Fallback to local player if global player not available
if (audioPlayer) {
audioPlayer.src = audioUrl;
audioPlayer.load();
audioPlayer.play();
isPlaying = true;
if (playIcon) playIcon.className = 'fas fa-pause';
if (playBtn) playBtn.classList.add('playing');
if (typeof recordPlay === 'function') {
recordPlay();
}
if (typeof showNotification === 'function') {
showNotification(`Now playing: ${title}`, 'info');
}
} else {
console.error('π΅ Audio player element not found for variation playback.');
if (typeof showNotification === 'function') {
showNotification('Failed to play variation.', 'error');
}
}
}
}
// Show Track Ratings Modal - displays all users who rated and their comments
function showTrackRatingsModal(trackId, trackTitle) {
const currentTrackId = trackId || <?= $track['id'] ?>;
const t = window.rankingTranslations || {};
console.log('β Showing ratings modal for track:', currentTrackId);
// Remove any existing modal
const existingModal = document.getElementById('trackRatingsViewModal');
if (existingModal) {
existingModal.remove();
}
// Create loading modal
const loadingModal = `
<div class="profile-edit-modal" id="trackRatingsViewModal" style="z-index: 10001;">
<div class="modal-content" style="max-width: 800px; max-height: 90vh; overflow-y: auto;">
<div class="modal-header">
<h3>${t.ratings_for || 'Ratings for:'} ${trackTitle}</h3>
<button class="close-btn" onclick="closeTrackRatingsModal()">×</button>
</div>
<div class="modal-body" style="text-align: center; padding: 3rem;">
<div class="loading-spinner" style="font-size: 3rem; margin-bottom: 1rem;">β³</div>
<p>${t.loading_ratings || 'Loading ratings...'}</p>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', loadingModal);
const modal = document.getElementById('trackRatingsViewModal');
if (modal) {
modal.style.display = 'flex';
}
// Fetch ratings from API
fetch(`/api_social.php?action=get_track_ratings&track_id=${currentTrackId}`)
.then(response => response.json())
.then(data => {
if (data.success && data.data) {
displayRatingsModal(data.data, trackTitle);
} else {
showNotification(data.message || 'Failed to load ratings', 'error');
closeTrackRatingsModal();
}
})
.catch(error => {
console.error('Error fetching ratings:', error);
showNotification('Failed to load ratings. Please try again.', 'error');
closeTrackRatingsModal();
});
}
function displayRatingsModal(ratingData, trackTitle) {
const modal = document.getElementById('trackRatingsViewModal');
if (!modal) return;
const t = window.rankingTranslations || {};
const { ratings, average_rating, total_ratings } = ratingData;
const currentTrackId = <?= $track['id'] ?>;
let ratingsHTML = '';
if (ratings.length === 0) {
ratingsHTML = `
<div style="text-align: center; padding: 3rem; color: #a0aec0;">
<div style="font-size: 4rem; margin-bottom: 1rem; opacity: 0.5;">β</div>
<h3 style="color: white; margin-bottom: 1rem;">No ratings yet</h3>
<p>Be the first to rate this track!</p>
</div>
`;
} else {
// Show summary
ratingsHTML = `
<div style="background: rgba(102, 126, 234, 0.1); border-radius: 12px; padding: 1.5rem; margin-bottom: 2rem; text-align: center;">
<div style="font-size: 2.5rem; font-weight: 700; color: #fbbf24; margin-bottom: 0.5rem;">
${average_rating}/10
</div>
<div style="color: #a0aec0; font-size: 1.1rem;">
Average from ${total_ratings} ${total_ratings === 1 ? 'rating' : 'ratings'}
</div>
</div>
<div style="max-height: 60vh; overflow-y: auto;">
`;
ratings.forEach(rating => {
const ratingDate = new Date(rating.created_at).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
// Get user initial for avatar
const userInitial = rating.user_name ? rating.user_name.charAt(0).toUpperCase() : '?';
ratingsHTML += `
<div style="background: rgba(255, 255, 255, 0.05); border-radius: 12px; padding: 1.5rem; margin-bottom: 1rem; border: 1px solid rgba(255, 255, 255, 0.1);">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
<div style="width: 50px; height: 50px; background: linear-gradient(135deg, #667eea, #764ba2); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-weight: 700; font-size: 1.2rem;">
${userInitial}
</div>
<div style="flex: 1;">
<div style="font-weight: 600; color: white; margin-bottom: 0.25rem;">
<a href="/artist_profile.php?id=${rating.user_id}" style="color: white; text-decoration: none;">
${rating.user_name || 'Anonymous'}
</a>
</div>
<div style="color: #a0aec0; font-size: 0.9rem;">
${ratingDate}
</div>
</div>
<div style="text-align: right;">
<div style="font-size: 1.5rem; font-weight: 700; color: #fbbf24;">
${rating.rating}/10
</div>
<div style="color: #a0aec0; font-size: 0.85rem;">
${getRatingLabel(rating.rating)}
</div>
</div>
</div>
${rating.comment ? `
<div style="background: rgba(255, 255, 255, 0.03); border-radius: 8px; padding: 1rem; color: #e2e8f0; line-height: 1.6; border-left: 3px solid #667eea;">
${rating.comment.replace(/\n/g, '<br>')}
</div>
` : `<div style="color: #718096; font-style: italic; font-size: 0.9rem;">${t.no_comment || 'No comment'}</div>`}
</div>
`;
});
ratingsHTML += '</div>';
}
modal.innerHTML = `
<div class="modal-content" style="max-width: 800px; max-height: 90vh; overflow-y: auto;">
<div class="modal-header">
<h3>${t.ratings_for || 'Ratings for:'} ${trackTitle}</h3>
<button class="close-btn" onclick="closeTrackRatingsModal()">×</button>
</div>
<div class="modal-body">
${ratingsHTML}
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeTrackRatingsModal()">${t.close || 'Close'}</button>
<button class="btn-primary" onclick="closeTrackRatingsModal(); setTimeout(() => { showTrackRatingModal(${currentTrackId}, '${trackTitle}'); }, 300);">
<i class="fas fa-star"></i> ${t.rate_this_track || 'Rate This Track'}
</button>
</div>
</div>
`;
}
function closeTrackRatingsModal() {
const modal = document.getElementById('trackRatingsViewModal');
if (modal) modal.remove();
}
function getRatingLabel(rating) {
const labels = {
1: 'Poor',
2: 'Below Average',
3: 'Fair',
4: 'Below Good',
5: 'Good',
6: 'Above Good',
7: 'Very Good',
8: 'Great',
9: 'Excellent',
10: 'Outstanding'
};
return labels[rating] || '';
}
// Track Rating Modal - for submitting ratings
function showTrackRatingModal(trackId, trackTitle) {
console.log('π΅ Track rating modal for:', trackId, trackTitle);
// Remove any existing track rating modal first
const existingModal = document.getElementById('trackRatingModal');
if (existingModal) {
existingModal.remove();
}
const modalHTML = `
<div class="profile-edit-modal" id="trackRatingModal" style="z-index: 10001;">
<div class="modal-content">
<div class="modal-header">
<h3>Rate Track: ${trackTitle}</h3>
<button class="close-btn" onclick="closeTrackRatingModal()">×</button>
</div>
<div class="modal-body">
<div class="rating-input-section">
<div class="stars-input">
<i class="far fa-star" data-rating="1"></i>
<i class="far fa-star" data-rating="2"></i>
<i class="far fa-star" data-rating="3"></i>
<i class="far fa-star" data-rating="4"></i>
<i class="far fa-star" data-rating="5"></i>
<i class="far fa-star" data-rating="6"></i>
<i class="far fa-star" data-rating="7"></i>
<i class="far fa-star" data-rating="8"></i>
<i class="far fa-star" data-rating="9"></i>
<i class="far fa-star" data-rating="10"></i>
</div>
<div class="rating-feedback">
<span class="rating-value">0/10</span>
<span class="rating-label">Click stars to rate this track</span>
</div>
</div>
<div class="rating-comment">
<label for="trackRatingComment">Add a comment (optional):</label>
<textarea id="trackRatingComment" placeholder="Share your thoughts about this track..." rows="4"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeTrackRatingModal()">Cancel</button>
<button class="btn-primary" onclick="submitTrackRating(${trackId})">Submit Rating</button>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
// Make sure modal is visible
const modal = document.getElementById('trackRatingModal');
if (modal) {
modal.style.display = 'flex';
modal.dataset.currentRating = '0'; // Initialize rating
console.log('β
Track rating modal created and visible');
} else {
console.error('β Failed to create track rating modal');
return;
}
// Fetch user's existing rating for this track
fetch(`/api_social.php?action=get_user_track_rating&track_id=${trackId}`)
.then(response => response.json())
.then(data => {
let existingRating = 0;
let existingComment = '';
if (data.success && data.data && data.data.rating) {
existingRating = data.data.rating;
existingComment = data.data.comment || '';
console.log('π΅ Found existing rating:', existingRating, 'comment:', existingComment);
}
// Initialize with existing rating if found
if (existingRating > 0) {
modal.dataset.currentRating = existingRating.toString();
// Update stars display
const stars = document.querySelectorAll('#trackRatingModal .stars-input i');
stars.forEach((s, index) => {
if (index < existingRating) {
s.className = 'fas fa-star';
} else {
s.className = 'far fa-star';
}
});
// Update feedback
document.querySelector('#trackRatingModal .rating-value').textContent = `${existingRating}/10`;
document.querySelector('#trackRatingModal .rating-label').textContent = getRatingLabel(existingRating);
// Set comment if exists
const commentTextarea = document.getElementById('trackRatingComment');
if (commentTextarea && existingComment) {
commentTextarea.value = existingComment;
}
}
// Add star rating functionality
const stars = document.querySelectorAll('#trackRatingModal .stars-input i');
let currentRating = existingRating;
stars.forEach(star => {
star.addEventListener('click', function() {
const rating = parseInt(this.dataset.rating);
currentRating = rating;
// Store rating in modal for submission
if (modal) {
modal.dataset.currentRating = rating.toString();
}
// Update stars display
stars.forEach((s, index) => {
if (index < rating) {
s.className = 'fas fa-star';
} else {
s.className = 'far fa-star';
}
});
// Update feedback
document.querySelector('#trackRatingModal .rating-value').textContent = `${rating}/10`;
document.querySelector('#trackRatingModal .rating-label').textContent = getRatingLabel(rating);
});
star.addEventListener('mouseenter', function() {
const rating = parseInt(this.dataset.rating);
stars.forEach((s, index) => {
if (index < rating) {
s.className = 'fas fa-star';
} else {
s.className = 'far fa-star';
}
});
});
star.addEventListener('mouseleave', function() {
stars.forEach((s, index) => {
if (index < currentRating) {
s.className = 'fas fa-star';
} else {
s.className = 'far fa-star';
}
});
});
});
})
.catch(error => {
console.error('Error fetching user rating:', error);
// Continue with empty rating if fetch fails
const stars = document.querySelectorAll('#trackRatingModal .stars-input i');
let currentRating = 0;
stars.forEach(star => {
star.addEventListener('click', function() {
const rating = parseInt(this.dataset.rating);
currentRating = rating;
if (modal) {
modal.dataset.currentRating = rating.toString();
}
stars.forEach((s, index) => {
if (index < rating) {
s.className = 'fas fa-star';
} else {
s.className = 'far fa-star';
}
});
document.querySelector('#trackRatingModal .rating-value').textContent = `${rating}/10`;
document.querySelector('#trackRatingModal .rating-label').textContent = getRatingLabel(rating);
});
star.addEventListener('mouseenter', function() {
const rating = parseInt(this.dataset.rating);
stars.forEach((s, index) => {
if (index < rating) {
s.className = 'fas fa-star';
} else {
s.className = 'far fa-star';
}
});
});
star.addEventListener('mouseleave', function() {
stars.forEach((s, index) => {
if (index < currentRating) {
s.className = 'fas fa-star';
} else {
s.className = 'far fa-star';
}
});
});
});
});
}
function closeTrackRatingModal() {
const modal = document.getElementById('trackRatingModal');
if (modal) modal.remove();
}
// Store rankings data globally for modal access
<?php if ($track['status'] === 'complete' && !empty($rankings)): ?>
window.trackRankings = <?= json_encode($rankings) ?>;
<?php
// CRITICAL SECURITY FIX: Sanitize track data before exposing to JavaScript
// Replace raw audio_url (which contains direct file paths like /audio_files/xxx.mp3)
// with signed URL that requires token validation
// Recursive function to remove all audio URLs from nested arrays/objects
function sanitizeAudioUrls($data) {
if (is_array($data) || is_object($data)) {
$sanitized = [];
foreach ($data as $key => $value) {
// Remove all audio URL fields (audio_url, source_audio_url, stream_audio_url, etc.)
if (preg_match('/audio_url|source_audio_url|stream_audio_url|source_stream_audio_url/i', $key)) {
continue; // Skip these fields
}
// Also check if the value itself is an audio URL string
if (is_string($value) && (
strpos($value, '.mp3') !== false ||
strpos($value, 'musicfile.api.box') !== false ||
strpos($value, 'cdn1.suno.ai') !== false ||
strpos($value, 'cdn2.suno.ai') !== false ||
strpos($value, '/audio_files/') !== false
)) {
continue; // Skip audio URL values
}
$sanitized[$key] = sanitizeAudioUrls($value); // Recursively sanitize nested data
}
return is_object($data) ? (object)$sanitized : $sanitized;
} elseif (is_string($data) && (
strpos($data, '.mp3') !== false ||
strpos($data, 'musicfile.api.box') !== false ||
strpos($data, 'cdn1.suno.ai') !== false ||
strpos($data, 'cdn2.suno.ai') !== false ||
strpos($data, '/audio_files/') !== false
)) {
// If it's a string containing audio URLs, return empty string instead of null
return '';
}
return $data;
}
$sanitizedTrack = $track;
$sanitizedTrack['audio_url'] = getSignedAudioUrl($track['id']); // Use signed URL instead of direct path
$sanitizedTrack['signed_audio_url'] = getSignedAudioUrl($track['id']); // Add signed URL as separate field for clarity
// Remove any other sensitive paths that might be exposed
unset($sanitizedTrack['source_audio_url']); // Don't expose source URLs
// CRITICAL: Sanitize metadata field which contains nested JSON with direct audio URLs
if (!empty($sanitizedTrack['metadata'])) {
$metadata = is_string($sanitizedTrack['metadata']) ? json_decode($sanitizedTrack['metadata'], true) : $sanitizedTrack['metadata'];
if (is_array($metadata)) {
$sanitizedTrack['metadata'] = json_encode(sanitizeAudioUrls($metadata), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
}
?>
window.currentTrack = <?= json_encode($sanitizedTrack) ?>;
<?php else: ?>
window.trackRankings = null;
window.currentTrack = null;
<?php endif; ?>
// Store translations for DMCA modal
window.dmcaTranslations = {
modal_title: '<?= addslashes(t('track.copyright.modal_title')) ?>',
modal_description: '<?= addslashes(t('track.copyright.modal_description')) ?>',
form_name: '<?= addslashes(t('track.copyright.form_name')) ?>',
form_email: '<?= addslashes(t('track.copyright.form_email')) ?>',
form_relationship: '<?= addslashes(t('track.copyright.form_relationship')) ?>',
form_relationship_owner: '<?= addslashes(t('track.copyright.form_relationship_owner')) ?>',
form_relationship_agent: '<?= addslashes(t('track.copyright.form_relationship_agent')) ?>',
form_relationship_other: '<?= addslashes(t('track.copyright.form_relationship_other')) ?>',
form_url: '<?= addslashes(t('track.copyright.form_url')) ?>',
form_url_placeholder: '<?= addslashes(t('track.copyright.form_url_placeholder')) ?>',
form_details: '<?= addslashes(t('track.copyright.form_details')) ?>',
form_details_placeholder: '<?= addslashes(t('track.copyright.form_details_placeholder')) ?>',
form_submit: '<?= addslashes(t('track.copyright.form_submit')) ?>',
form_cancel: '<?= addslashes(t('track.copyright.form_cancel')) ?>',
form_required: '<?= addslashes(t('track.copyright.form_required')) ?>',
submit_success: '<?= addslashes(t('track.copyright.submit_success')) ?>',
submit_error: '<?= addslashes(t('track.copyright.submit_error')) ?>',
submitting: '<?= addslashes(t('track.copyright.submitting')) ?>'
};
// Store translations for modal
window.rankingTranslations = {
title: '<?= addslashes(t('track.rankings.breakdown.title')) ?>',
overall_ranking: '<?= addslashes(t('track.rankings.breakdown.overall_ranking')) ?>',
rank: '<?= addslashes(t('track.rankings.breakdown.rank')) ?>',
out_of: '<?= addslashes(t('track.rankings.breakdown.out_of')) ?>',
tracks: '<?= addslashes(t('track.rankings.breakdown.tracks')) ?>',
percentile: '<?= addslashes(t('track.rankings.breakdown.percentile')) ?>',
score_calculation: '<?= addslashes(t('track.rankings.breakdown.score_calculation')) ?>',
plays: '<?= addslashes(t('track.rankings.breakdown.plays')) ?>',
likes: '<?= addslashes(t('track.rankings.breakdown.likes')) ?>',
rating: '<?= addslashes(t('track.rankings.breakdown.rating')) ?>',
total_score: '<?= addslashes(t('track.rankings.breakdown.total_score')) ?>',
individual_rankings: '<?= addslashes(t('track.rankings.breakdown.individual_rankings')) ?>',
formula: '<?= addslashes(t('track.rankings.breakdown.formula')) ?>',
formula_text: '<?= addslashes(t('track.rankings.breakdown.formula_text')) ?>',
weight: '<?= addslashes(t('track.rankings.breakdown.weight')) ?>',
avg_rating: '<?= addslashes(t('track.rankings.breakdown.avg_rating')) ?>',
ratings: '<?= addslashes(t('track.rankings.breakdown.ratings')) ?>',
close: '<?= addslashes(t('track.rankings.breakdown.close')) ?>',
view_all_rankings: '<?= addslashes(t('track.rankings.breakdown.view_all_rankings')) ?>',
all_rankings_title: '<?= addslashes(t('track.rankings.breakdown.all_rankings_title')) ?>',
loading_rankings: '<?= addslashes(t('track.rankings.breakdown.loading_rankings')) ?>',
track: '<?= addslashes(t('track.rankings.breakdown.track')) ?>',
showing: '<?= addslashes(t('track.rankings.breakdown.showing')) ?>',
no_tracks: '<?= addslashes(t('track.rankings.breakdown.no_tracks')) ?>',
by_plays: '<?= addslashes(t('track.rankings.by_plays')) ?>',
by_likes: '<?= addslashes(t('track.rankings.by_likes')) ?>',
by_rating: '<?= addslashes(t('track.rankings.by_rating')) ?>',
of: '<?= addslashes(t('track.rankings.of')) ?>',
ratings_for: '<?= addslashes(t('artist_profile.ratings_for')) ?>',
loading_ratings: '<?= addslashes(t('artist_profile.loading_ratings')) ?>',
no_comment: '<?= addslashes(t('artist_profile.no_comment')) ?>',
rate_this_track: '<?= addslashes(t('artist_profile.rate_this_track')) ?>'
};
// Simple function to open ranking breakdown
function openRankingBreakdown() {
console.log('π openRankingBreakdown called');
console.log('π Rankings data:', window.trackRankings);
console.log('π΅ Track data:', window.currentTrack);
if (!window.trackRankings || !window.currentTrack) {
console.error('β Rankings or track data missing');
alert('Ranking data is not available for this track.');
return;
}
try {
showRankingBreakdown(window.trackRankings, window.currentTrack);
} catch (error) {
console.error('β Error opening ranking breakdown:', error);
alert('Error opening ranking breakdown: ' + error.message);
}
}
function submitTrackRating(trackId) {
// Get rating from modal data attribute or parse from text
const modal = document.getElementById('trackRatingModal');
let rating = 0;
if (modal && modal.dataset.currentRating) {
rating = parseInt(modal.dataset.currentRating);
} else {
// Fallback: parse from text content
const ratingText = document.querySelector('#trackRatingModal .rating-value');
if (ratingText) {
const ratingStr = ratingText.textContent.split('/')[0].trim();
rating = parseInt(ratingStr);
}
}
const comment = document.getElementById('trackRatingComment')?.value || '';
// Validate rating
if (!rating || rating < 1 || rating > 10) {
showNotification('Please select a rating between 1 and 10', 'warning');
return;
}
console.log('Submitting track rating:', { trackId, rating, comment });
// Send track rating to API
const formData = new FormData();
formData.append('action', 'submit_track_rating');
formData.append('track_id', trackId);
formData.append('rating', rating);
formData.append('comment', comment);
fetch('/api_social.php', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.status);
}
return response.json();
})
.then(data => {
console.log('Track rating submission response:', data);
if (data.success) {
showNotification('Track rating submitted successfully!', 'success');
closeTrackRatingModal();
// Refresh the page to show updated rating
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
const errorMsg = data.message || 'Failed to submit track rating';
console.error('Track rating submission failed:', errorMsg);
showNotification(errorMsg, 'error');
}
})
.catch(error => {
console.error('Track rating submission error:', error);
showNotification('Failed to submit track rating: ' + error.message, 'error');
});
}
// Show Ranking Breakdown Modal
function showRankingBreakdown(rankings, track) {
console.log('π showRankingBreakdown called with:', rankings, track);
if (!rankings || !track) {
console.error('β Missing rankings or track data');
alert('Missing data to display ranking breakdown.');
return;
}
console.log('β
Data validated, creating modal...');
// Remove any existing modal
const existingModal = document.getElementById('rankingBreakdownModal');
if (existingModal) {
existingModal.remove();
}
const breakdown = rankings.score_breakdown || {};
const plays = parseInt(track.play_count) || 0;
const likes = parseInt(track.like_count) || 0;
const avgRating = parseFloat(track.average_rating) || 0;
const ratingCount = parseInt(track.rating_count) || 0;
const t = window.rankingTranslations || {};
const modalHTML = `
<div class="profile-edit-modal" id="rankingBreakdownModal" style="z-index: 10001;">
<div class="modal-content" style="max-width: 600px;">
<div class="modal-header">
<h3>π ${t.title || 'Ranking Breakdown'}: ${track.title || 'Track'}</h3>
<button class="close-btn" onclick="closeRankingBreakdownModal()">×</button>
</div>
<div class="modal-body">
<div style="margin-bottom: 2rem;">
<h4 style="color: #667eea; margin-bottom: 1rem;">${t.overall_ranking || 'Overall Ranking'}</h4>
<div style="background: rgba(0, 0, 0, 0.2); padding: 1rem; border-radius: 8px; margin-bottom: 1rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
<span style="color: #a0aec0;">${t.rank || ''}:</span>
<span style="font-size: 1.5rem; font-weight: 700; color: #fbbf24;">#${rankings.overall}</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="color: #a0aec0;">${t.out_of || 'Out of'}:</span>
<span style="color: #ffffff;">${rankings.total_tracks} ${t.tracks || 'tracks'}</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 0.5rem;">
<span style="color: #a0aec0;">${t.percentile || 'Percentile'}:</span>
<span style="color: #ffffff;">Top ${rankings.overall_percentile}%</span>
</div>
</div>
</div>
<div style="margin-bottom: 2rem;">
<h4 style="color: #667eea; margin-bottom: 1rem;">${t.score_calculation || 'Score Calculation'}</h4>
<div style="background: rgba(0, 0, 0, 0.2); padding: 1rem; border-radius: 8px;">
<div style="margin-bottom: 0.75rem; padding-bottom: 0.75rem; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
<div style="display: flex; justify-content: space-between; margin-bottom: 0.25rem;">
<span style="color: #a0aec0;">${t.plays || 'Plays'} (${plays} Γ 1.0):</span>
<span style="color: #ffffff; font-weight: 600;">${breakdown.plays_score || 0}</span>
</div>
<small style="color: #888; font-size: 0.85rem;">${plays} ${t.plays || 'plays'} Γ ${t.weight || 'weight'} 1.0</small>
</div>
<div style="margin-bottom: 0.75rem; padding-bottom: 0.75rem; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
<div style="display: flex; justify-content: space-between; margin-bottom: 0.25rem;">
<span style="color: #a0aec0;">${t.likes || 'Likes'} (${likes} Γ 2.0):</span>
<span style="color: #ffffff; font-weight: 600;">${breakdown.likes_score || 0}</span>
</div>
<small style="color: #888; font-size: 0.85rem;">${likes} ${t.likes || 'likes'} Γ ${t.weight || 'weight'} 2.0</small>
</div>
<div style="margin-bottom: 0.75rem; padding-bottom: 0.75rem; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
<div style="display: flex; justify-content: space-between; margin-bottom: 0.25rem;">
<span style="color: #a0aec0;">${t.rating || 'Rating'} (${(Number(avgRating) || 0).toFixed(1)} Γ ${ratingCount} Γ 5.0):</span>
<span style="color: #ffffff; font-weight: 600;">${breakdown.rating_score || 0}</span>
</div>
<small style="color: #888; font-size: 0.85rem;">${(Number(avgRating) || 0).toFixed(1)} ${t.avg_rating || 'avg rating'} Γ ${ratingCount} ${t.ratings || 'ratings'} Γ ${t.weight || 'weight'} 5.0</small>
</div>
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 2px solid rgba(251, 191, 36, 0.3);">
<div style="display: flex; justify-content: space-between;">
<span style="color: #fbbf24; font-weight: 700; font-size: 1.1rem;">${t.total_score || 'Total Score'}:</span>
<span style="color: #fbbf24; font-weight: 700; font-size: 1.3rem;">${breakdown.total_score || 0}</span>
</div>
</div>
</div>
</div>
<div style="margin-bottom: 2rem;">
<h4 style="color: #667eea; margin-bottom: 1rem;">${t.individual_rankings || 'Individual Rankings'}</h4>
<div style="display: grid; gap: 0.75rem;">
<div style="background: rgba(0, 0, 0, 0.2); padding: 0.75rem; border-radius: 8px; display: flex; justify-content: space-between;">
<span style="color: #a0aec0;">${t.by_plays || 'By Plays'}:</span>
<span style="color: #ffffff; font-weight: 600;">#${rankings.plays} ${t.of || 'of'} ${rankings.total_tracks}</span>
</div>
<div style="background: rgba(0, 0, 0, 0.2); padding: 0.75rem; border-radius: 8px; display: flex; justify-content: space-between;">
<span style="color: #a0aec0;">${t.by_likes || 'By Likes'}:</span>
<span style="color: #ffffff; font-weight: 600;">#${rankings.likes} ${t.of || 'of'} ${rankings.total_tracks}</span>
</div>
${ratingCount >= 3 && rankings.rating ? `
<div style="background: rgba(0, 0, 0, 0.2); padding: 0.75rem; border-radius: 8px; display: flex; justify-content: space-between;">
<span style="color: #a0aec0;">${t.by_rating || 'By Rating'}:</span>
<span style="color: #ffffff; font-weight: 600;">#${rankings.rating}</span>
</div>
` : ''}
</div>
</div>
<div style="background: rgba(102, 126, 234, 0.1); padding: 1rem; border-radius: 8px; border-left: 3px solid #667eea;">
<small style="color: #a0aec0; font-size: 0.85rem;">
<strong>${t.formula || 'Formula'}:</strong> ${t.formula_text || 'Total Score = (Plays Γ 1) + (Likes Γ 2) + (Avg Rating Γ Rating Count Γ 5)'}
</small>
</div>
<div style="margin-top: 2rem; padding-top: 2rem; border-top: 2px solid rgba(255, 255, 255, 0.1);">
<button class="btn-primary" onclick="showAllTrackRankings()" style="width: 100%; padding: 12px; font-size: 1rem; font-weight: 600;">
<i class="fas fa-list"></i> ${t.view_all_rankings || 'View All Track Rankings'}
</button>
</div>
</div>
<div class="modal-footer">
<button class="btn-primary" onclick="closeRankingBreakdownModal()">${t.close || 'Close'}</button>
</div>
</div>
</div>
`;
console.log('π Inserting modal HTML...');
document.body.insertAdjacentHTML('beforeend', modalHTML);
// Wait a bit for DOM to update
setTimeout(() => {
const modal = document.getElementById('rankingBreakdownModal');
console.log('π Modal element found:', modal);
if (modal) {
// Modal should already have styles from CSS class, but ensure it's visible
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
console.log('β
Modal displayed successfully');
// Add click outside to close
modal.addEventListener('click', function(e) {
if (e.target === modal) {
closeRankingBreakdownModal();
}
});
} else {
console.error('β Modal not found after insertion');
alert('Failed to create modal. Please refresh the page.');
}
}, 10);
}
function closeRankingBreakdownModal() {
const modal = document.getElementById('rankingBreakdownModal');
if (modal) {
modal.remove();
document.body.style.overflow = '';
}
}
// Show All Track Rankings Modal
function showAllTrackRankings() {
// Close the current modal
closeRankingBreakdownModal();
// Create loading modal
const loadingModal = `
<div class="profile-edit-modal" id="allRankingsModal" style="z-index: 10002;">
<div class="modal-content" style="max-width: 900px; max-height: 90vh;">
<div class="modal-header">
<h3>π ${window.rankingTranslations?.all_rankings_title || 'All Track Rankings'}</h3>
<button class="close-btn" onclick="closeAllRankingsModal()">×</button>
</div>
<div class="modal-body" style="text-align: center; padding: 3rem;">
<div class="loading-spinner" style="font-size: 3rem; margin-bottom: 1rem;">β³</div>
<p>${window.rankingTranslations?.loading_rankings || 'Loading rankings...'}</p>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', loadingModal);
const modal = document.getElementById('allRankingsModal');
if (modal) {
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
// Fetch all rankings
fetch('/api/get_all_track_rankings.php?per_page=500')
.then(response => response.json())
.then(data => {
if (data.success && data.tracks) {
displayAllRankings(data.tracks, data.pagination);
} else {
if (typeof showNotification === 'function') {
showNotification(data.error || 'Failed to load rankings', 'error');
} else {
alert(data.error || 'Failed to load rankings');
}
closeAllRankingsModal();
}
})
.catch(error => {
console.error('Error fetching rankings:', error);
if (typeof showNotification === 'function') {
showNotification('Failed to load rankings. Please try again.', 'error');
} else {
alert('Failed to load rankings. Please try again.');
}
closeAllRankingsModal();
});
}
function displayAllRankings(tracks, pagination) {
const modal = document.getElementById('allRankingsModal');
if (!modal) return;
const t = window.rankingTranslations || {};
const currentTrackId = window.currentTrack?.id;
let tracksHTML = '';
if (tracks.length === 0) {
tracksHTML = `
<div style="text-align: center; padding: 3rem; color: #a0aec0;">
<p>${t.no_tracks || 'No tracks found'}</p>
</div>
`;
} else {
tracksHTML = `
<div style="overflow-x: auto; max-height: 60vh;">
<table style="width: 100%; border-collapse: collapse; color: white;">
<thead>
<tr style="background: rgba(102, 126, 234, 0.2); border-bottom: 2px solid rgba(102, 126, 234, 0.5);">
<th style="padding: 12px; text-align: left; font-weight: 700; color: #667eea;">${t.rank || ''}</th>
<th style="padding: 12px; text-align: left; font-weight: 700; color: #667eea;">${t.track || 'Track'}</th>
<th style="padding: 12px; text-align: center; font-weight: 700; color: #667eea;">${t.plays || 'Plays'}</th>
<th style="padding: 12px; text-align: center; font-weight: 700; color: #667eea;">${t.likes || 'Likes'}</th>
<th style="padding: 12px; text-align: center; font-weight: 700; color: #667eea;">${t.rating || 'Rating'}</th>
<th style="padding: 12px; text-align: right; font-weight: 700; color: #fbbf24;">${t.total_score || 'Score'}</th>
</tr>
</thead>
<tbody>
${tracks.map((track, index) => {
const isCurrentTrack = track.id == currentTrackId;
const rowStyle = isCurrentTrack
? 'background: rgba(251, 191, 36, 0.2); border-left: 3px solid #fbbf24;'
: index % 2 === 0
? 'background: rgba(255, 255, 255, 0.05);'
: 'background: rgba(255, 255, 255, 0.02);';
return `
<tr style="${rowStyle}">
<td style="padding: 12px; font-weight: ${isCurrentTrack ? '700' : '600'}; color: ${isCurrentTrack ? '#fbbf24' : '#ffffff'};">
#${track.rank}
</td>
<td style="padding: 12px;">
<a href="/track.php?id=${track.id}" style="color: ${isCurrentTrack ? '#fbbf24' : '#667eea'}; text-decoration: none; font-weight: ${isCurrentTrack ? '700' : '500'};"
onmouseover="this.style.textDecoration='underline'"
onmouseout="this.style.textDecoration='none'">
${escapeHtmlForModal(track.title)}
</a>
<div style="font-size: 0.85rem; color: #a0aec0; margin-top: 2px;">
${escapeHtmlForModal(track.artist_name)}
</div>
</td>
<td style="padding: 12px; text-align: center; color: #a0aec0;">
${track.play_count.toLocaleString()}
</td>
<td style="padding: 12px; text-align: center; color: #a0aec0;">
${track.like_count.toLocaleString()}
</td>
<td style="padding: 12px; text-align: center; color: #a0aec0;">
${track.rating_count > 0 ? track.avg_rating.toFixed(1) + ' (' + track.rating_count + ')' : '-'}
</td>
<td style="padding: 12px; text-align: right; font-weight: 600; color: ${isCurrentTrack ? '#fbbf24' : '#ffffff'};">
${track.total_score.toLocaleString()}
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
${pagination && pagination.total_tracks > pagination.per_page ? `
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid rgba(255, 255, 255, 0.1); text-align: center; color: #a0aec0;">
${t.showing || 'Showing'} ${((pagination.page - 1) * pagination.per_page) + 1}-${Math.min(pagination.page * pagination.per_page, pagination.total_tracks)} ${t.of || 'of'} ${pagination.total_tracks.toLocaleString()} ${t.tracks || 'tracks'}
</div>
` : ''}
`;
}
modal.querySelector('.modal-body').innerHTML = tracksHTML;
// Add click outside to close
modal.addEventListener('click', function(e) {
if (e.target === modal) {
closeAllRankingsModal();
}
});
}
function closeAllRankingsModal() {
const modal = document.getElementById('allRankingsModal');
if (modal) {
modal.remove();
document.body.style.overflow = '';
}
}
function escapeHtmlForModal(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// DMCA Report Modal
function openDMCAReportModal(trackId) {
const t = window.dmcaTranslations || {};
const modal = document.createElement('div');
modal.id = 'dmcaReportModal';
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
backdrop-filter: blur(5px);
`;
modal.innerHTML = `
<div style="
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 15px;
padding: 2rem;
max-width: 600px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
border: 2px solid rgba(102, 126, 234, 0.3);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2 style="margin: 0; color: #ffffff; font-size: 1.5rem;">
<i class="fas fa-exclamation-triangle" style="color: #fca5a5; margin-right: 0.5rem;"></i>
${t.modal_title || 'Report Copyright Infringement'}
</h2>
<button onclick="closeDMCAReportModal()" style="
background: transparent;
border: none;
color: #a0aec0;
font-size: 1.5rem;
cursor: pointer;
padding: 0.5rem;
transition: color 0.3s;
" onmouseover="this.style.color='#ffffff'" onmouseout="this.style.color='#a0aec0'">
<i class="fas fa-times"></i>
</button>
</div>
<p style="color: #a0aec0; margin-bottom: 1.5rem; line-height: 1.6;">
${t.modal_description || 'If you believe this track is being used without permission, please fill out the form below. We take copyright infringement seriously and will investigate all reports.'}
</p>
<form id="dmcaReportForm" onsubmit="submitDMCAReport(event, ${trackId})">
<div style="margin-bottom: 1.25rem;">
<label style="display: block; color: #ffffff; margin-bottom: 0.5rem; font-weight: 500;">
${t.form_name || 'Your Name'} <span style="color: #fca5a5;">${t.form_required || '*'}</span>
</label>
<input type="text" name="reporter_name" required style="
width: 100%;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
color: #ffffff;
font-size: 1rem;
">
</div>
<div style="margin-bottom: 1.25rem;">
<label style="display: block; color: #ffffff; margin-bottom: 0.5rem; font-weight: 500;">
${t.form_email || 'Your Email'} <span style="color: #fca5a5;">${t.form_required || '*'}</span>
</label>
<input type="email" name="reporter_email" required style="
width: 100%;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
color: #ffffff;
font-size: 1rem;
">
</div>
<div style="margin-bottom: 1.25rem;">
<label style="display: block; color: #ffffff; margin-bottom: 0.5rem; font-weight: 500;">
${t.form_relationship || 'Your Relationship'} <span style="color: #fca5a5;">${t.form_required || '*'}</span>
</label>
<select name="reporter_relationship" required style="
width: 100%;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
color: #ffffff;
font-size: 1rem;
">
<option value="owner">${t.form_relationship_owner || 'Track Owner'}</option>
<option value="authorized_agent">${t.form_relationship_agent || 'Authorized Agent'}</option>
<option value="other">${t.form_relationship_other || 'Other'}</option>
</select>
</div>
<div style="margin-bottom: 1.25rem;">
<label style="display: block; color: #ffffff; margin-bottom: 0.5rem; font-weight: 500;">
${t.form_url || 'Infringing URL'} <span style="color: #fca5a5;">${t.form_required || '*'}</span>
</label>
<input type="url" name="infringing_url" required placeholder="${t.form_url_placeholder || 'https://example.com/infringing-content'}" style="
width: 100%;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
color: #ffffff;
font-size: 1rem;
">
</div>
<div style="margin-bottom: 1.5rem;">
<label style="display: block; color: #ffffff; margin-bottom: 0.5rem; font-weight: 500;">
${t.form_details || 'Additional Details'}
</label>
<textarea name="description" rows="4" placeholder="${t.form_details_placeholder || 'Please provide any additional information...'}" style="
width: 100%;
padding: 0.75rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
color: #ffffff;
font-size: 1rem;
resize: vertical;
font-family: inherit;
"></textarea>
</div>
<div style="display: flex; gap: 1rem;">
<button type="submit" style="
flex: 1;
padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 8px;
color: #ffffff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;
" onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform='translateY(0)'">
${t.form_submit || 'Submit Report'}
</button>
<button type="button" onclick="closeDMCAReportModal()" style="
padding: 0.75rem 1.5rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
color: #ffffff;
font-size: 1rem;
cursor: pointer;
">
${t.form_cancel || 'Cancel'}
</button>
</div>
</form>
</div>
`;
document.body.appendChild(modal);
document.body.style.overflow = 'hidden';
// Close on outside click
modal.addEventListener('click', function(e) {
if (e.target === modal) {
closeDMCAReportModal();
}
});
}
function closeDMCAReportModal() {
const modal = document.getElementById('dmcaReportModal');
if (modal) {
modal.remove();
document.body.style.overflow = '';
}
}
function submitDMCAReport(event, trackId) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const data = {
track_id: trackId,
reporter_name: formData.get('reporter_name'),
reporter_email: formData.get('reporter_email'),
reporter_relationship: formData.get('reporter_relationship'),
infringing_url: formData.get('infringing_url'),
description: formData.get('description') || ''
};
const t = window.dmcaTranslations || {};
const submitBtn = form.querySelector('button[type="submit"]');
const originalText = submitBtn.textContent;
submitBtn.disabled = true;
submitBtn.textContent = t.submitting || 'Submitting...';
fetch('/api/submit_dmca_report.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
alert(t.submit_success || 'Thank you! Your DMCA report has been submitted. We will review it shortly.');
closeDMCAReportModal();
} else {
alert('Error: ' + (result.message || 'Failed to submit report. Please try again.'));
submitBtn.disabled = false;
submitBtn.textContent = originalText;
}
})
.catch(error => {
console.error('DMCA Report Error:', error);
alert(t.submit_error || 'An error occurred. Please try again.');
submitBtn.disabled = false;
submitBtn.textContent = originalText;
});
}
</script>
</div>
<?php if ($user_id): ?>
<!-- Add to Crate Modal -->
<div id="addToCrateModal" class="modal-overlay" style="display: none;">
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header">
<h3><i class="fas fa-box"></i> <?= t('library.crates.add_to_crate') ?></h3>
<button class="modal-close" onclick="closeAddToCrateModal()">×</button>
</div>
<div class="modal-body">
<p id="addToCrateTrackTitle" style="color: #a5b4fc; margin-bottom: 1rem;"></p>
<div id="cratesList" style="max-height: 300px; overflow-y: auto;">
<div style="text-align: center; padding: 2rem;">
<i class="fas fa-spinner fa-spin"></i> <?= t('library.crates.loading') ?>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeAddToCrateModal()"><?= t('common.cancel', 'Cancel') ?></button>
<a href="/library.php?tab=crates" class="btn btn-primary" style="text-decoration: none;">
<i class="fas fa-plus"></i> <?= t('library.crates.create_new') ?>
</a>
</div>
</div>
</div>
<style>
#addToCrateModal.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(8px);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
}
#addToCrateModal .modal-content {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 16px;
padding: 0;
max-width: 400px;
width: 90%;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.1);
}
#addToCrateModal .modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
#addToCrateModal .modal-header h3 { margin: 0; color: white; font-size: 1.25rem; }
#addToCrateModal .modal-close {
background: none;
border: none;
color: #9ca3af;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
line-height: 1;
}
#addToCrateModal .modal-close:hover { color: white; }
#addToCrateModal .modal-body { padding: 1.5rem; }
#addToCrateModal .modal-footer {
display: flex;
justify-content: flex-end;
gap: 1rem;
padding: 1rem 1.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
#addToCrateModal .crate-option {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
background: rgba(255,255,255,0.05);
border-radius: 8px;
margin-bottom: 0.5rem;
cursor: pointer;
transition: all 0.2s ease;
}
#addToCrateModal .crate-option:hover { background: rgba(102, 126, 234, 0.2); }
#addToCrateModal .crate-option .crate-info { display: flex; align-items: center; gap: 0.75rem; }
#addToCrateModal .crate-option .crate-icon { font-size: 1.5rem; }
#addToCrateModal .crate-option .crate-name { font-weight: 600; color: white; }
#addToCrateModal .crate-option .crate-count { font-size: 0.85rem; color: #9ca3af; }
#addToCrateModal .crate-option.adding { opacity: 0.6; pointer-events: none; }
</style>
<script>
let currentTrackIdForCrate = null;
let currentTrackTitleForCrate = null;
function openAddToCrateModal(trackId, trackTitle) {
currentTrackIdForCrate = trackId;
currentTrackTitleForCrate = trackTitle;
const modal = document.getElementById('addToCrateModal');
const titleEl = document.getElementById('addToCrateTrackTitle');
titleEl.textContent = '"' + trackTitle + '"';
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
// Load user's crates
loadUserCratesForModal();
}
function closeAddToCrateModal() {
const modal = document.getElementById('addToCrateModal');
modal.style.display = 'none';
document.body.style.overflow = '';
currentTrackIdForCrate = null;
currentTrackTitleForCrate = null;
}
function loadUserCratesForModal() {
const cratesList = document.getElementById('cratesList');
cratesList.innerHTML = '<div style="text-align: center; padding: 2rem;"><i class="fas fa-spinner fa-spin"></i> <?= addslashes(t('library.crates.loading')) ?></div>';
fetch('/api/get_user_crates.php')
.then(response => response.json())
.then(data => {
if (data.success && data.crates && data.crates.length > 0) {
cratesList.innerHTML = data.crates.map(crate => `
<div class="crate-option" onclick="addTrackToCrateFromModal(${crate.id}, '${escapeHtmlAttr(crate.name)}')">
<div class="crate-info">
<span class="crate-icon">π¦</span>
<div>
<div class="crate-name">${escapeHtml(crate.name)}</div>
<div class="crate-count">${crate.track_count || 0} <?= t('library.crates.tracks') ?></div>
</div>
</div>
<i class="fas fa-plus" style="color: #667eea;"></i>
</div>
`).join('');
} else {
cratesList.innerHTML = '<div style="text-align: center; padding: 2rem; color: #9ca3af;"><?= addslashes(t('library.crates.no_crates')) ?></div>';
}
})
.catch(error => {
console.error('Error loading crates:', error);
cratesList.innerHTML = '<div style="text-align: center; padding: 2rem; color: #ef4444;">Error loading crates</div>';
});
}
function addTrackToCrateFromModal(crateId, crateName) {
const crateOption = event.currentTarget;
crateOption.classList.add('adding');
crateOption.innerHTML = '<div class="crate-info"><i class="fas fa-spinner fa-spin"></i> Adding...</div>';
fetch('/api/add_track_to_crate.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
crate_id: crateId,
track_id: currentTrackIdForCrate
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof showNotification === 'function') {
showNotification('<?= addslashes(t('library.crates.track_added_to')) ?>'.replace(':crate', crateName), 'success');
}
closeAddToCrateModal();
} else {
if (typeof showNotification === 'function') {
showNotification(data.error || 'Failed to add track', 'error');
}
// Reload the crates list to show updated state
loadUserCratesForModal();
}
})
.catch(error => {
console.error('Error adding track to crate:', error);
if (typeof showNotification === 'function') {
showNotification('Failed to add track to crate', 'error');
}
loadUserCratesForModal();
});
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function escapeHtmlAttr(text) {
if (!text) return '';
return text.replace(/'/g, "\\'").replace(/"/g, '\\"');
}
// Close modal on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeAddToCrateModal();
}
});
// Close modal on background click
document.getElementById('addToCrateModal')?.addEventListener('click', function(e) {
if (e.target === this) {
closeAddToCrateModal();
}
});
// Ensure add to crate button is clickable - add backup event listener
document.addEventListener('DOMContentLoaded', function() {
// Find all add to crate buttons and ensure they're clickable
const addToCrateButtons = document.querySelectorAll('.action-btn.secondary-btn');
addToCrateButtons.forEach(button => {
const icon = button.querySelector('.fa-box');
if (icon) {
// This is the add to crate button
// Ensure it has proper styles
button.style.pointerEvents = 'auto';
button.style.cursor = 'pointer';
button.style.zIndex = '11';
button.style.position = 'relative';
// Add backup click handler that gets track info from URL and page
button.addEventListener('click', function(e) {
// Check if the function exists
if (typeof openAddToCrateModal === 'function') {
// Get track ID from URL
const urlParams = new URLSearchParams(window.location.search);
const trackId = parseInt(urlParams.get('id'));
// Get track title from page
const trackTitleEl = document.getElementById('trackTitle');
const trackTitle = trackTitleEl ? trackTitleEl.textContent.trim() : '';
if (trackId && trackTitle) {
// Only call if the inline onclick didn't work (check if modal opened)
setTimeout(function() {
const modal = document.getElementById('addToCrateModal');
if (modal && modal.style.display === 'none') {
// Modal didn't open, so the inline onclick didn't work - call it manually
openAddToCrateModal(trackId, trackTitle);
}
}, 50);
}
}
}, true); // Use capture phase to ensure it fires
}
});
});
</script>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>