![]() 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/private_html/ |
<?php
// Define development mode (set to true for debugging, false for production)
define('DEVELOPMENT_MODE', false);
// Disable error reporting in production for security
error_reporting(0);
ini_set('display_errors', 0);
session_start();
// Include translation system
require_once __DIR__ . '/includes/translations.php';
// Include audio token system for signed URLs
require_once __DIR__ . '/utils/audio_token.php';
// Include security functions for session management
require_once __DIR__ . '/includes/security.php';
// Add security headers
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
header('Referrer-Policy: strict-origin-when-cross-origin');
// ALWAYS check if user is logged in - no exceptions
if (!isset($_SESSION['user_id'])) {
header('Location: /auth/login.php');
exit;
}
// Note: secureSession() is automatically called when includes/security.php is included above
// This updates last_activity and checks for timeout
// Debug: Log that we're past the session check (only in development)
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("Library.php: Session check passed, user_id: " . ($_SESSION['user_id'] ?? 'not set'));
}
// Handle track edit form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'update_track') {
// SECURITY: CSRF Protection
$csrf_token = $_POST['csrf_token'] ?? '';
if (!validateCSRFToken($csrf_token)) {
error_log("SECURITY: CSRF token validation failed in library.php from IP: " . ($_SERVER['REMOTE_ADDR'] ?? 'unknown'));
$_SESSION['error_message'] = 'Security validation failed. Please refresh the page and try again.';
header('Location: /library.php');
exit;
}
// Include database configuration
require_once 'config/database.php';
try {
// Get database connection
$pdo = getDBConnection();
if (!$pdo) {
throw new Exception('Database connection failed');
}
// SECURITY: Validate and sanitize track_id
$track_id_raw = $_POST['track_id'] ?? null;
if (!$track_id_raw || !is_numeric($track_id_raw) || (int)$track_id_raw <= 0) {
error_log("SECURITY: Invalid track_id attempt in library.php: " . htmlspecialchars($track_id_raw ?? '', ENT_QUOTES, 'UTF-8'));
throw new Exception('Invalid track ID');
}
$track_id = (int)$track_id_raw;
// SECURITY: Validate and sanitize title
$title = trim($_POST['title'] ?? '');
if (empty($title)) {
throw new Exception('Title is required');
}
// SECURITY: Limit title length to prevent DoS
$title = substr($title, 0, 200);
// Apply title case capitalization (e.g., "dreaming of u" → "Dreaming Of U")
$title = mb_convert_case($title, MB_CASE_TITLE, 'UTF-8');
// SECURITY: Validate and sanitize description
$description = trim($_POST['description'] ?? '');
// SECURITY: Limit description length to prevent DoS
$description = substr($description, 0, 5000);
$price = floatval($_POST['price'] ?? 0);
$is_public = isset($_POST['is_public']) ? 1 : 0;
// Validate price is one of the allowed tiers: Free, $0.99, $1.99, $2.99
$allowed_prices = [0.00, 0.99, 1.99, 2.99];
$original_price = $price;
// Round price to 2 decimal places first
$price = round($price, 2);
// Check if price matches exactly (with tolerance for floating point)
$matched_price = null;
foreach ($allowed_prices as $ap) {
if (abs($price - $ap) < 0.005) { // 0.005 tolerance for rounding
$matched_price = $ap;
break;
}
}
// Use matched price or default to free if not matching
$price = $matched_price !== null ? $matched_price : 0.00;
// Validate that the track belongs to the current user
$stmt = $pdo->prepare("SELECT user_id, is_public, published_at FROM music_tracks WHERE id = ?");
$stmt->execute([$track_id]);
$track = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$track || $track['user_id'] != $_SESSION['user_id']) {
$_SESSION['error_message'] = 'You can only edit your own tracks.';
header('Location: /library.php');
exit;
}
// Determine if we need to set published_at (first time going public)
$set_published_at = false;
if ($is_public == 1 && $track['is_public'] == 0 && empty($track['published_at'])) {
$set_published_at = true;
}
// Update the track
// Note: The database column is 'prompt', not 'description'
if ($set_published_at) {
// First time publishing - set permanent published_at timestamp
$stmt = $pdo->prepare("
UPDATE music_tracks
SET title = ?, prompt = ?, price = ?, is_public = ?, published_at = NOW(), updated_at = NOW()
WHERE id = ? AND user_id = ?
");
} else {
// Not first publish - don't touch published_at
$stmt = $pdo->prepare("
UPDATE music_tracks
SET title = ?, prompt = ?, price = ?, is_public = ?, updated_at = NOW()
WHERE id = ? AND user_id = ?
");
}
$result = $stmt->execute([$title, $description, $price, $is_public, $track_id, $_SESSION['user_id']]);
if ($result) {
$_SESSION['success_message'] = 'Track updated successfully!';
} else {
$_SESSION['error_message'] = 'Failed to update track. Please try again.';
}
} catch (Exception $e) {
// Always log errors for debugging
error_log("Error updating track: " . $e->getMessage());
error_log("Error trace: " . $e->getTraceAsString());
$_SESSION['error_message'] = 'An error occurred while updating the track: ' . htmlspecialchars($e->getMessage());
} catch (Error $e) {
// Catch fatal errors too
error_log("Fatal error updating track: " . $e->getMessage());
error_log("Error trace: " . $e->getTraceAsString());
$_SESSION['error_message'] = 'A fatal error occurred while updating the track.';
}
header('Location: /library.php');
exit;
}
// Helper functions for formatting
function formatBytes($bytes, $precision = 2) {
if ($bytes == 0) return '0 B';
$units = ['B', 'KB', 'MB', 'GB'];
$base = log($bytes, 1024);
$unit = $units[floor($base)];
return round(pow(1024, $base - floor($base)), $precision) . ' ' . $unit;
}
function formatTime($seconds) {
if ($seconds < 60) {
return round($seconds, 1) . 's';
} elseif ($seconds < 3600) {
$minutes = floor($seconds / 60);
$remainingSeconds = $seconds % 60;
return $minutes . 'm ' . round($remainingSeconds) . 's';
} else {
$hours = floor($seconds / 3600);
$minutes = floor(($seconds % 3600) / 60);
return $hours . 'h ' . $minutes . 'm';
}
}
// Include database configuration
require_once 'config/database.php';
// Debug: Test database connection (only in development)
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("Library.php: Database config loaded");
}
// Get user info with error handling
$user_id = $_SESSION['user_id'] ?? null; // Define user_id for JavaScript checks
$session_id = session_id(); // Define session_id for signed URLs
try {
$user = getUserById($_SESSION['user_id']);
$user_name = $user['name'] ?? 'User';
// Get credits from database, not session (session can be outdated)
$credits = $user['credits'] ?? 0;
// Update session with current credits for consistency
$_SESSION['credits'] = $credits;
} catch (Exception $e) {
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("Error getting user info: " . $e->getMessage());
}
$user_name = 'User';
$credits = $_SESSION['credits'] ?? 5;
}
// Get subscription info if user has subscription
$subscription_info = null;
$subscription_usage = null;
try {
require_once __DIR__ . '/utils/subscription_helpers.php';
$subscription_info = getSubscriptionInfo($_SESSION['user_id']);
if ($subscription_info) {
$subscription_usage = getMonthlyTrackUsage($_SESSION['user_id'], $subscription_info['plan_name'] ?? null);
}
// FIX: Use getEffectivePlan() to get the correct plan (checks subscription first, then users.plan)
$plan = getEffectivePlan($_SESSION['user_id']);
$_SESSION['plan'] = $plan;
} catch (Exception $e) {
error_log("Error getting subscription info in library.php: " . $e->getMessage());
$subscription_info = null;
$subscription_usage = null;
// Fallback to users.plan if getEffectivePlan fails
$plan = $user['plan'] ?? 'free';
$_SESSION['plan'] = $plan;
}
// Get user stats
$pdo = getDBConnection();
if (!$pdo) {
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("Database connection failed in library.php");
}
die("Unable to connect to the database. Please try again later.");
}
$stmt = $pdo->prepare("
SELECT
COUNT(*) as total_tracks,
COUNT(CASE WHEN status = 'complete' THEN 1 END) as completed_tracks,
COUNT(CASE WHEN status = 'processing' THEN 1 END) as processing_tracks,
COUNT(CASE WHEN status = 'failed' THEN 1 END) as failed_tracks,
AVG(CASE WHEN status = 'complete' THEN duration END) as avg_duration,
SUM(CASE WHEN status = 'complete' THEN duration END) as total_duration
FROM music_tracks
WHERE user_id = ? AND user_id IS NOT NULL
");
try {
$stmt->execute([$_SESSION['user_id']]);
$user_stats = $stmt->fetch();
// Debug: Log the actual count for troubleshooting
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("Library stats for user_id " . $_SESSION['user_id'] . ": total_tracks = " . ($user_stats['total_tracks'] ?? 0));
}
} catch (Exception $e) {
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("Error getting user stats: " . $e->getMessage());
}
$user_stats = [
'total_tracks' => 0,
'completed_tracks' => 0,
'processing_tracks' => 0,
'failed_tracks' => 0,
'avg_duration' => 0,
'total_duration' => 0
];
}
// Get credit transaction history (last 10)
$credit_history = [];
try {
$stmt = $pdo->prepare("
SELECT * FROM credit_transactions
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT 10
");
$stmt->execute([$_SESSION['user_id']]);
$credit_history = $stmt->fetchAll();
} catch (Exception $e) {
// Credit history table might not exist yet
}
// Calculate user level based on activity
$user_level = 1;
$xp = ($user_stats['completed_tracks'] ?? 0) * 10 + ($user_stats['total_duration'] ?? 0);
if ($xp >= 1000) $user_level = 5;
elseif ($xp >= 500) $user_level = 4;
elseif ($xp >= 200) $user_level = 3;
elseif ($xp >= 50) $user_level = 2;
// Get recent activity (last 10 tracks)
$recent_activity = [];
try {
$stmt = $pdo->prepare("
SELECT title, created_at, status
FROM music_tracks
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT 10
");
$stmt->execute([$_SESSION['user_id']]);
$recent_activity = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
// Error getting recent activity
}
// Get user's music tracks with enhanced data and variations
// SECURITY: Validate and sanitize GET parameters
$status_filter_raw = $_GET['status'] ?? 'all';
$sort_filter_raw = $_GET['sort'] ?? 'latest';
$time_filter_raw = $_GET['time'] ?? 'all';
$genre_filter_raw = $_GET['genre'] ?? '';
$search_query_raw = trim($_GET['search'] ?? '');
// SECURITY: Validate status filter against allowed values
$allowed_statuses = ['all', 'complete', 'processing', 'failed', 'pending'];
$status_filter = in_array($status_filter_raw, $allowed_statuses) ? $status_filter_raw : 'all';
// SECURITY: Validate sort filter against allowed values
$allowed_sorts = ['latest', 'oldest', 'popular', 'most-played'];
$sort_filter = in_array($sort_filter_raw, $allowed_sorts) ? $sort_filter_raw : 'latest';
// SECURITY: Validate time filter against allowed values
$allowed_times = ['all', 'today', 'week', 'month'];
$time_filter = in_array($time_filter_raw, $allowed_times) ? $time_filter_raw : 'all';
// SECURITY: Sanitize genre filter (alphanumeric, spaces, hyphens only)
$genre_filter = preg_replace('/[^a-zA-Z0-9\s\-]/', '', $genre_filter_raw);
$genre_filter = substr($genre_filter, 0, 50); // Limit length
// SECURITY: Sanitize search query (remove dangerous characters, limit length)
$search_query = preg_replace('/[<>"\']/', '', $search_query_raw);
$search_query = substr($search_query, 0, 100); // Limit length
// Build WHERE clause for status filtering
$where_clause = "WHERE mt.user_id = ?";
$params = [$_SESSION['user_id']];
if ($status_filter !== 'all') {
$where_clause .= " AND mt.status = ?";
$params[] = $status_filter;
}
// Add search filter
if (!empty($search_query)) {
$where_clause .= " AND (mt.title LIKE ? OR mt.prompt LIKE ?)";
$search_param = '%' . $search_query . '%';
$params[] = $search_param;
$params[] = $search_param;
}
// Build time filter condition and add to WHERE clause
switch ($time_filter) {
case 'today':
$where_clause .= " AND mt.created_at >= CURDATE()";
break;
case 'week':
$where_clause .= " AND mt.created_at >= DATE_SUB(NOW(), INTERVAL 1 WEEK)";
break;
case 'month':
$where_clause .= " AND mt.created_at >= DATE_SUB(NOW(), INTERVAL 1 MONTH)";
break;
case 'all':
default:
// No time filter
break;
}
// Build genre filter condition and add to WHERE clause
if (!empty($genre_filter) && $genre_filter !== 'all') {
// Try to match genre from metadata JSON field
$where_clause .= " AND (JSON_UNQUOTE(JSON_EXTRACT(mt.metadata, '$.genre')) = ? OR mt.genre = ?)";
$params[] = $genre_filter;
$params[] = $genre_filter;
}
// Build ORDER BY clause and additional JOINs for sorting
$order_clause = "ORDER BY ";
$additional_joins = "";
switch ($sort_filter) {
case 'oldest':
$order_clause .= "mt.created_at ASC";
break;
case 'popular':
$additional_joins = "LEFT JOIN (SELECT track_id, COUNT(*) as like_count FROM track_likes GROUP BY track_id) likes ON mt.id = likes.track_id";
$order_clause = "ORDER BY COALESCE(likes.like_count, 0) DESC, mt.created_at DESC";
break;
case 'most-played':
$additional_joins = "LEFT JOIN (SELECT track_id, COUNT(*) as play_count FROM track_plays GROUP BY track_id) plays ON mt.id = plays.track_id";
$order_clause = "ORDER BY COALESCE(plays.play_count, 0) DESC, mt.created_at DESC";
break;
case 'latest':
default:
$order_clause .= "mt.created_at DESC";
break;
}
// Get tracks
try {
// Check if audio_variations table exists
$checkTable = $pdo->query("SHOW TABLES LIKE 'audio_variations'");
if ($checkTable->rowCount() > 0) {
$stmt = $pdo->prepare("
SELECT
mt.*,
COALESCE(vars.variation_count, 0) as variation_count,
CASE
WHEN mt.created_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR) THEN '🔥 Hot'
WHEN mt.created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) THEN '⭐ New'
ELSE ''
END as badge
FROM music_tracks mt
LEFT JOIN (
SELECT track_id, COUNT(*) as variation_count
FROM audio_variations
GROUP BY track_id
) vars ON mt.id = vars.track_id
$additional_joins
$where_clause
$order_clause
");
} else {
// Fallback query without audio_variations table
$stmt = $pdo->prepare("
SELECT
mt.*,
0 as variation_count,
CASE
WHEN mt.created_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR) THEN '🔥 Hot'
WHEN mt.created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR) THEN '⭐ New'
ELSE ''
END as badge
FROM music_tracks mt
$additional_joins
$where_clause
$order_clause
");
}
} catch (Exception $e) {
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("Error preparing tracks query: " . $e->getMessage());
}
// Fallback to simple query
$stmt = $pdo->prepare("
SELECT
mt.*,
0 as variation_count,
'' as badge
FROM music_tracks mt
$where_clause
ORDER BY mt.created_at DESC
");
}
try {
$stmt->execute($params);
$user_tracks = $stmt->fetchAll();
} catch (Exception $e) {
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("Error getting user tracks: " . $e->getMessage());
}
$user_tracks = [];
}
// OPTIMIZED: Batch load variations for user_tracks instead of N+1 queries
$tracks_with_variations = [];
if (!empty($user_tracks)) {
// Check if audio_variations table exists
$checkTable = $pdo->query("SHOW TABLES LIKE 'audio_variations'");
$has_variations_table = $checkTable->rowCount() > 0;
$variations_by_track = [];
if ($has_variations_table) {
// Get all track IDs that have variations
$track_ids_with_variations = [];
foreach ($user_tracks as $idx => $track) {
if (($track['variation_count'] ?? 0) > 0) {
$track_ids_with_variations[] = $track['id'];
}
}
if (!empty($track_ids_with_variations)) {
$placeholders = implode(',', array_fill(0, count($track_ids_with_variations), '?'));
$variations_stmt = $pdo->prepare("
SELECT
track_id,
variation_index,
audio_url,
duration,
title,
tags,
image_url,
source_audio_url,
metadata,
created_at
FROM audio_variations
WHERE track_id IN ($placeholders)
ORDER BY track_id, variation_index ASC
");
$variations_stmt->execute($track_ids_with_variations);
$all_variations = $variations_stmt->fetchAll(PDO::FETCH_ASSOC);
// Group variations by track_id
foreach ($all_variations as $variation) {
$variations_by_track[$variation['track_id']][] = $variation;
}
}
}
$user_id = $_SESSION['user_id'] ?? null;
$session_id = session_id();
foreach ($user_tracks as $track) {
$track['variations'] = [];
if (($track['variation_count'] ?? 0) > 0 && isset($variations_by_track[$track['id']])) {
$raw_variations = $variations_by_track[$track['id']];
// Convert audio URLs to signed proxy URLs for security
foreach ($raw_variations as &$variation) {
if (isset($variation['variation_index'])) {
// Replace raw audio_url with signed proxy URL
$variation['audio_url'] = getSignedAudioUrl($track['id'], $variation['variation_index'], null, $user_id, $session_id);
}
// Ensure duration is properly set - check metadata if duration is null/0
if (empty($variation['duration']) || $variation['duration'] == 0) {
if (!empty($variation['metadata'])) {
$meta = is_string($variation['metadata']) ? json_decode($variation['metadata'], true) : $variation['metadata'];
if (isset($meta['duration']) && $meta['duration'] !== null && $meta['duration'] !== '' && $meta['duration'] > 0) {
$variation['duration'] = (float)$meta['duration'];
}
}
// Fallback: use main track duration if variation duration is still missing
if (empty($variation['duration']) || $variation['duration'] == 0) {
$variation['duration'] = (float)($track['duration'] ?? 0);
}
} else {
// Ensure duration is a number (could be stored as string)
$variation['duration'] = (float)$variation['duration'];
}
// Remove source URLs for security
unset($variation['source_audio_url']);
}
unset($variation); // Break reference
$track['variations'] = $raw_variations;
}
$tracks_with_variations[] = $track;
}
}
// CRITICAL FIX: Check if "failed" tracks are actually still processing
// If a track has a valid task_id and was created recently, it might still be processing
foreach ($tracks_with_variations as &$track) {
// If status is 'failed' but has valid task_id and is recent, treat as potentially processing
if ($track['status'] === 'failed') {
$task_id = $track['task_id'] ?? $track['api_task_id'] ?? null;
$created_at = strtotime($track['created_at']);
$hours_old = (time() - $created_at) / 3600;
// If track has valid task_id and is less than 2 hours old, it might still be processing
if ($task_id &&
$task_id !== 'unknown' &&
!str_starts_with($task_id, 'temp_') &&
!str_starts_with($task_id, 'retry_') &&
$hours_old < 2) {
// Override status to 'processing' to prevent showing Retry/Delete buttons
$track['status'] = 'processing';
$track['_was_failed_but_checking'] = true; // Flag for display
}
}
// If status is NULL or empty, treat as processing
if (empty($track['status']) || $track['status'] === null) {
$track['status'] = 'processing';
}
}
unset($track);
// Also protect recent_tracks if they exist
if (isset($recent_tracks) && !empty($recent_tracks)) {
foreach ($recent_tracks as &$track) {
if ($track['status'] === 'failed') {
$task_id = $track['task_id'] ?? $track['api_task_id'] ?? null;
$created_at = strtotime($track['created_at']);
$hours_old = (time() - $created_at) / 3600;
if ($task_id &&
$task_id !== 'unknown' &&
!str_starts_with($task_id, 'temp_') &&
!str_starts_with($task_id, 'retry_') &&
$hours_old < 2) {
$track['status'] = 'processing';
$track['_was_failed_but_checking'] = true;
}
}
if (empty($track['status']) || $track['status'] === null) {
$track['status'] = 'processing';
}
}
unset($track);
}
// Set page variables for header
$current_page = 'library';
$page_title = t('library.page_title');
$page_description = t('library.page_description');
include 'includes/header.php';
// Enhanced sort order function with proper handling
function getSortOrder($sort_filter) {
// Debug logging
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("🎵 Library: Sorting by '$sort_filter'");
}
switch ($sort_filter) {
case 'latest':
return "mt.created_at DESC";
case 'oldest':
return "mt.created_at ASC";
case 'popular':
// OPTIMIZED: Use JOIN alias instead of subquery
return "COALESCE(like_stats.like_count, 0) DESC, mt.created_at DESC";
case 'most-played':
// OPTIMIZED: Use JOIN alias instead of subquery
return "COALESCE(play_stats.play_count, 0) DESC, mt.created_at DESC";
case 'most-viewed':
// OPTIMIZED: Use JOIN alias instead of subquery
return "COALESCE(view_stats.view_count, 0) DESC, mt.created_at DESC";
case 'most-commented':
// OPTIMIZED: Use JOIN alias instead of subquery
return "COALESCE(comment_stats.comment_count, 0) DESC, mt.created_at DESC";
case 'most-shared':
// OPTIMIZED: Use JOIN alias instead of subquery
return "COALESCE(share_stats.share_count, 0) DESC, mt.created_at DESC";
case 'title':
return "mt.title ASC";
case 'artist':
return "u.name ASC, mt.created_at DESC";
case 'duration':
return "mt.duration DESC, mt.created_at DESC";
default:
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
error_log("🎵 Library: Unknown sort filter '$sort_filter', using default");
}
return "mt.created_at DESC";
}
}
// Pagination setup
$per_page = 50; // Optimized: Load 50 tracks per page instead of 1000
$current_page = max(1, intval($_GET['page'] ?? 1));
$offset = ($current_page - 1) * $per_page;
// Get total count for pagination
try {
$count_query = "
SELECT COUNT(*) as total
FROM music_tracks mt
" . $where_clause . "
AND (
(mt.status = 'complete' AND mt.audio_url IS NOT NULL AND mt.audio_url != '')
OR
(mt.status = 'processing' AND mt.task_id IS NOT NULL)
OR
(mt.status = 'failed')
)
";
$count_stmt = $pdo->prepare($count_query);
$count_stmt->execute($params);
$total_tracks = (int)$count_stmt->fetch(PDO::FETCH_ASSOC)['total'];
$total_pages = ceil($total_tracks / $per_page);
} catch (Exception $e) {
$total_tracks = 0;
$total_pages = 1;
}
// Get user's tracks with social data and artist info (safe query)
try {
// Build the query using the WHERE clause that filters by user_id
// Check if audio_variations table exists for variation count
$checkTable = $pdo->query("SHOW TABLES LIKE 'audio_variations'");
$has_variations_table = $checkTable->rowCount() > 0;
$variation_count_select = $has_variations_table
? "COALESCE(variation_stats.variation_count, 0) as variation_count"
: "0 as variation_count";
$variation_join = $has_variations_table
? "LEFT JOIN (SELECT track_id, COUNT(*) as variation_count FROM audio_variations GROUP BY track_id) variation_stats ON mt.id = variation_stats.track_id\n "
: "";
// OPTIMIZED: Using JOINs instead of correlated subqueries for better performance
$query = "
SELECT
mt.id,
mt.title,
mt.prompt,
mt.audio_url,
mt.duration,
mt.created_at,
mt.user_id,
mt.price,
mt.metadata,
mt.status,
mt.task_id,
COALESCE(u.name, 'Unknown Artist') as artist_name,
u.profile_image,
COALESCE(u.plan, 'free') as plan,
$variation_count_select,
COALESCE(like_stats.like_count, 0) as like_count,
COALESCE(comment_stats.comment_count, 0) as comment_count,
COALESCE(share_stats.share_count, 0) as share_count,
COALESCE(play_stats.play_count, 0) as play_count,
COALESCE(view_stats.view_count, 0) as view_count,
COALESCE((SELECT COUNT(*) FROM user_follows WHERE follower_id = ? AND following_id = mt.user_id), 0) as is_following,
CASE WHEN user_like_stats.track_id IS NOT NULL THEN 1 ELSE 0 END as user_liked,
COALESCE(artist_stats.total_tracks, 0) as artist_total_tracks
FROM music_tracks mt
LEFT JOIN users u ON mt.user_id = u.id
LEFT JOIN (SELECT track_id, COUNT(*) as like_count FROM track_likes GROUP BY track_id) like_stats ON mt.id = like_stats.track_id
LEFT JOIN (SELECT track_id, COUNT(*) as comment_count FROM track_comments GROUP BY track_id) comment_stats ON mt.id = comment_stats.track_id
LEFT JOIN (SELECT track_id, COUNT(*) as share_count FROM track_shares GROUP BY track_id) share_stats ON mt.id = share_stats.track_id
LEFT JOIN (SELECT track_id, COUNT(*) as play_count FROM track_plays GROUP BY track_id) play_stats ON mt.id = play_stats.track_id
LEFT JOIN (SELECT track_id, COUNT(*) as view_count FROM track_views GROUP BY track_id) view_stats ON mt.id = view_stats.track_id
LEFT JOIN (SELECT user_id, COUNT(*) as total_tracks FROM music_tracks WHERE status = 'complete' GROUP BY user_id) artist_stats ON mt.user_id = artist_stats.user_id
LEFT JOIN (SELECT track_id FROM track_likes WHERE user_id = ?) user_like_stats ON mt.id = user_like_stats.track_id
" . $variation_join . "
" . $where_clause . "
AND (
(mt.status = 'complete' AND mt.audio_url IS NOT NULL AND mt.audio_url != '')
OR
(mt.status = 'processing' AND mt.task_id IS NOT NULL)
OR
(mt.status = 'failed')
)
ORDER BY " . getSortOrder($sort_filter) . "
LIMIT ? OFFSET ?
";
$stmt = $pdo->prepare($query);
// Add user_id parameters for is_following and user_liked checks
// $params already contains the user_id from $where_clause, so we add it twice more for the subqueries
// Use $_SESSION['user_id'] directly to ensure we have the correct user ID for like checking
$check_user_id = isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : 0;
// Debug: Log the user_id being used for like checking
if (empty($recent_tracks) || (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE)) {
error_log("Library query - Session user_id: " . ($_SESSION['user_id'] ?? 'NULL') . ", check_user_id: " . $check_user_id . ", params count: " . count($params));
}
$execute_params = array_merge($params, [$check_user_id, $check_user_id, $per_page, $offset]);
$stmt->execute($execute_params);
$recent_tracks = $stmt->fetchAll();
// Ensure user_liked is an integer (PDO might return it as string)
// OPTIMIZED: Removed redundant like check - main query already includes this via CASE WHEN
foreach ($recent_tracks as &$track) {
$track['user_liked'] = (int)($track['user_liked'] ?? 0);
}
unset($track); // Break reference
// OPTIMIZED: Batch load variations instead of N+1 queries
$checkTable = $pdo->query("SHOW TABLES LIKE 'audio_variations'");
$has_variations_table = $checkTable->rowCount() > 0;
if ($has_variations_table && !empty($recent_tracks)) {
// Get all track IDs
$track_ids = array_column($recent_tracks, 'id');
$placeholders = implode(',', array_fill(0, count($track_ids), '?'));
// Batch load all variations in one query
$variations_stmt = $pdo->prepare("
SELECT
track_id,
variation_index,
audio_url,
duration,
title,
tags,
image_url,
source_audio_url,
metadata,
created_at
FROM audio_variations
WHERE track_id IN ($placeholders)
ORDER BY track_id, variation_index ASC
");
$variations_stmt->execute($track_ids);
$all_variations = $variations_stmt->fetchAll(PDO::FETCH_ASSOC);
// Group variations by track_id
$variations_by_track = [];
foreach ($all_variations as $variation) {
$variations_by_track[$variation['track_id']][] = $variation;
}
// Also get task_ids for tracks that might share variations
$task_ids = array_filter(array_column($recent_tracks, 'task_id'));
$main_tracks_by_task = [];
if (!empty($task_ids)) {
$task_placeholders = implode(',', array_fill(0, count($task_ids), '?'));
$main_track_stmt = $pdo->prepare("
SELECT task_id, id
FROM music_tracks
WHERE task_id IN ($task_placeholders) AND status = 'complete'
ORDER BY created_at ASC, id ASC
");
$main_track_stmt->execute($task_ids);
$main_tracks = $main_track_stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($main_tracks as $mt) {
if (!isset($main_tracks_by_task[$mt['task_id']])) {
$main_tracks_by_task[$mt['task_id']] = $mt['id'];
}
}
}
// Assign variations to tracks
$user_id = $_SESSION['user_id'] ?? null;
$session_id = session_id();
foreach ($recent_tracks as &$track) {
$track['variations'] = [];
// First, try to get variations for this track
$raw_variations = $variations_by_track[$track['id']] ?? [];
// Convert audio URLs to signed proxy URLs
foreach ($raw_variations as &$variation) {
if (isset($variation['variation_index'])) {
$variation['audio_url'] = getSignedAudioUrl($track['id'], $variation['variation_index'], null, $user_id, $session_id);
}
// Ensure duration is properly set - check metadata if duration is null/0
if (empty($variation['duration']) || $variation['duration'] == 0) {
if (!empty($variation['metadata'])) {
$meta = is_string($variation['metadata']) ? json_decode($variation['metadata'], true) : $variation['metadata'];
if (isset($meta['duration']) && $meta['duration'] !== null && $meta['duration'] !== '' && $meta['duration'] > 0) {
$variation['duration'] = (float)$meta['duration'];
}
}
// Fallback: use main track duration if variation duration is still missing
if (empty($variation['duration']) || $variation['duration'] == 0) {
$variation['duration'] = (float)($track['duration'] ?? 0);
}
} else {
$variation['duration'] = (float)$variation['duration'];
}
unset($variation['source_audio_url']);
}
unset($variation);
$track['variations'] = $raw_variations;
// If no variations found for this track, but it has a task_id,
// check if there's a main track (first track with same task_id) that has variations
if (empty($track['variations']) && !empty($track['task_id']) && isset($main_tracks_by_task[$track['task_id']])) {
$main_track_id = $main_tracks_by_task[$track['task_id']];
// If main track exists and is different, get its variations from batch
if ($main_track_id != $track['id']) {
$raw_variations = $variations_by_track[$main_track_id] ?? [];
// Convert audio URLs to signed proxy URLs
foreach ($raw_variations as &$variation) {
if (isset($variation['variation_index'])) {
$variation['audio_url'] = getSignedAudioUrl($track['id'], $variation['variation_index'], null, $user_id, $session_id);
}
// Ensure duration is properly set - check metadata if duration is null/0
if (empty($variation['duration']) || $variation['duration'] == 0) {
if (!empty($variation['metadata'])) {
$meta = is_string($variation['metadata']) ? json_decode($variation['metadata'], true) : $variation['metadata'];
if (isset($meta['duration']) && $meta['duration'] !== null && $meta['duration'] !== '' && $meta['duration'] > 0) {
$variation['duration'] = (float)$meta['duration'];
}
}
// Fallback: use main track duration if variation duration is still missing
if (empty($variation['duration']) || $variation['duration'] == 0) {
$variation['duration'] = (float)($track['duration'] ?? 0);
}
} else {
$variation['duration'] = (float)$variation['duration'];
}
unset($variation['source_audio_url']);
}
unset($variation);
$track['variations'] = $raw_variations;
}
}
// Update variation_count based on actual variations found
$track['variation_count'] = count($track['variations']);
}
unset($track); // Break reference
} else {
// No variations table, set variation_count to 0
foreach ($recent_tracks as &$track) {
$track['variations'] = [];
$track['variation_count'] = $track['variation_count'] ?? 0;
}
unset($track);
}
// Debug: Log first track's user_liked value if in development mode
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE && !empty($recent_tracks)) {
error_log("Library: First track user_liked=" . $recent_tracks[0]['user_liked'] . ", track_id=" . $recent_tracks[0]['id'] . ", session_user_id=" . ($_SESSION['user_id'] ?? 'NULL') . ", check_user_id=" . $check_user_id);
if (!empty($recent_tracks[0]['variations'])) {
error_log("Library: First track has " . count($recent_tracks[0]['variations']) . " variations, variation_count=" . ($recent_tracks[0]['variation_count'] ?? 0));
}
}
} catch (Exception $e) {
// Fallback to basic query if social features fail
$checkTable = $pdo->query("SHOW TABLES LIKE 'audio_variations'");
$has_variations_table = $checkTable->rowCount() > 0;
$variation_count_select = $has_variations_table
? "COALESCE((SELECT COUNT(*) FROM audio_variations WHERE track_id = mt.id), 0) as variation_count"
: "0 as variation_count";
$stmt = $pdo->prepare("
SELECT
mt.id,
mt.title,
mt.prompt,
mt.audio_url,
mt.duration,
mt.created_at,
mt.user_id,
mt.price,
mt.metadata,
mt.status,
mt.task_id,
COALESCE(u.name, 'Unknown Artist') as artist_name,
u.profile_image,
COALESCE(u.plan, 'free') as plan,
$variation_count_select,
0 as like_count,
0 as comment_count,
0 as share_count,
0 as play_count,
0 as view_count,
0 as is_following,
0 as user_liked,
1 as artist_total_tracks
FROM music_tracks mt
LEFT JOIN users u ON mt.user_id = u.id
WHERE mt.status = 'complete'
AND mt.audio_url IS NOT NULL
AND mt.audio_url != ''
ORDER BY mt.created_at DESC
LIMIT ? OFFSET ?
");
$stmt->execute([$per_page, $offset]);
$recent_tracks = $stmt->fetchAll();
// Load variations for fallback tracks too
if ($has_variations_table) {
foreach ($recent_tracks as &$track) {
$track['variations'] = [];
// Get variations for this track
$stmt = $pdo->prepare("
SELECT
variation_index,
audio_url,
duration,
title,
tags,
image_url,
source_audio_url,
metadata,
created_at
FROM audio_variations
WHERE track_id = ?
ORDER BY variation_index ASC
");
$stmt->execute([$track['id']]);
$raw_variations = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Convert audio URLs to signed proxy URLs
foreach ($raw_variations as &$variation) {
if (isset($variation['variation_index'])) {
$variation['audio_url'] = getSignedAudioUrl($track['id'], $variation['variation_index'], null, $user_id, $session_id);
}
// Ensure duration is properly set - check metadata if duration is null/0
if (empty($variation['duration']) || $variation['duration'] == 0) {
if (!empty($variation['metadata'])) {
$meta = is_string($variation['metadata']) ? json_decode($variation['metadata'], true) : $variation['metadata'];
if (isset($meta['duration']) && $meta['duration'] !== null && $meta['duration'] !== '' && $meta['duration'] > 0) {
$variation['duration'] = (float)$meta['duration'];
}
}
// Fallback: use main track duration if variation duration is still missing
if (empty($variation['duration']) || $variation['duration'] == 0) {
$variation['duration'] = (float)($track['duration'] ?? 0);
}
} else {
$variation['duration'] = (float)$variation['duration'];
}
unset($variation['source_audio_url']);
}
unset($variation);
$track['variations'] = $raw_variations;
// If no variations and has task_id, check main track
if (empty($track['variations']) && !empty($track['task_id'])) {
$main_track_stmt = $pdo->prepare("
SELECT id
FROM music_tracks
WHERE task_id = ? AND status = 'complete'
ORDER BY created_at ASC, id ASC
LIMIT 1
");
$main_track_stmt->execute([$track['task_id']]);
$main_track = $main_track_stmt->fetch(PDO::FETCH_ASSOC);
if ($main_track && $main_track['id'] != $track['id']) {
$stmt->execute([$main_track['id']]);
$raw_variations = $stmt->fetchAll();
// Convert audio URLs to signed proxy URLs
foreach ($raw_variations as &$variation) {
if (isset($variation['variation_index'])) {
$variation['audio_url'] = getSignedAudioUrl($track['id'], $variation['variation_index'], null, $user_id, $session_id);
}
// Ensure duration is properly set - check metadata if duration is null/0
if (empty($variation['duration']) || $variation['duration'] == 0) {
if (!empty($variation['metadata'])) {
$meta = is_string($variation['metadata']) ? json_decode($variation['metadata'], true) : $variation['metadata'];
if (isset($meta['duration']) && $meta['duration'] !== null && $meta['duration'] !== '' && $meta['duration'] > 0) {
$variation['duration'] = (float)$meta['duration'];
}
}
// Fallback: use main track duration if variation duration is still missing
if (empty($variation['duration']) || $variation['duration'] == 0) {
$variation['duration'] = (float)($track['duration'] ?? 0);
}
} else {
$variation['duration'] = (float)$variation['duration'];
}
unset($variation['source_audio_url']);
}
unset($variation);
$track['variations'] = $raw_variations;
}
}
$track['variation_count'] = count($track['variations']);
}
unset($track);
} else {
foreach ($recent_tracks as &$track) {
$track['variations'] = [];
$track['variation_count'] = 0;
}
unset($track);
}
}
// Get all available genres from user's tracks
$genres_query = $pdo->prepare("
SELECT DISTINCT COALESCE(
JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.genre')),
mt.genre,
'Electronic'
) as genre
FROM music_tracks mt
WHERE mt.user_id = ?
AND (
JSON_EXTRACT(metadata, '$.genre') IS NOT NULL
OR mt.genre IS NOT NULL
)
AND COALESCE(
JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.genre')),
mt.genre,
''
) != ''
ORDER BY genre ASC
");
$genres_query->execute([$_SESSION['user_id']]);
$available_genres = $genres_query->fetchAll(PDO::FETCH_COLUMN);
// Add popular genres if missing from database
$popular_genres = ['Electronic', 'House', 'Techno', 'Pop', 'Hip Hop', 'Rock', 'Jazz', 'Classical', 'Ambient', 'Trance', 'Dubstep', 'R&B', 'Reggae', 'Country', 'Folk', 'Blues', 'Funk', 'Disco', 'Drum & Bass', 'Progressive', 'Chillout', 'Lofi'];
foreach ($popular_genres as $genre) {
if (!in_array($genre, $available_genres)) {
$available_genres[] = $genre;
}
}
$available_genres = array_unique($available_genres);
sort($available_genres);
// Get total count for pagination
$count_stmt = $pdo->prepare("
SELECT COUNT(*) as total
FROM music_tracks mt
WHERE mt.status = 'complete'
AND mt.audio_url IS NOT NULL
AND mt.audio_url != ''
");
$count_stmt->execute();
$total_tracks = $count_stmt->fetch()['total'];
$total_pages = ceil($total_tracks / $per_page);
// Get community stats
$community_stats = [
'total_tracks' => $total_tracks,
'total_artists' => count(array_unique(array_column($recent_tracks, 'artist_name'))),
'total_likes' => 0,
'total_comments' => 0,
'total_shares' => 0
];
?>
<div class="main-content">
<style>
/* ============================================
NEXT-GEN LIBRARY - OPTIMIZED & MODERN DESIGN
Performance-first, beautiful, cutting-edge
============================================ */
/* CSS Variables for easy theming */
:root {
--primary: #667eea;
--secondary: #764ba2;
--accent: #f59e0b;
--success: #48bb78;
--danger: #ef4444;
--bg-dark: #0a0a0a;
--bg-medium: #1a1a1a;
--bg-light: #2d2d2d;
--text-primary: #ffffff;
--text-secondary: #a0aec0;
--border: rgba(255, 255, 255, 0.1);
--glass: rgba(255, 255, 255, 0.05);
--shadow: rgba(102, 126, 234, 0.1);
}
/* Reset & Base */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
padding-bottom: 120px;
background: var(--bg-dark);
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', 'Roboto', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: 1.6;
}
/* Performance: GPU acceleration for animations */
.hero, .library-content, .track-card, .stat-card {
will-change: transform;
transform: translateZ(0);
backface-visibility: hidden;
}
/* ============================================
HERO SECTION - Modern & Bold
============================================ */
.hero {
padding: 10rem 0 8rem;
text-align: center;
background: linear-gradient(135deg, var(--bg-dark) 0%, var(--bg-medium) 50%, var(--bg-dark) 100%);
position: relative;
overflow: hidden;
margin-bottom: 0;
}
.hero::before {
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(circle at 20% 50%, rgba(102, 126, 234, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(118, 75, 162, 0.1) 0%, transparent 50%),
url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(102,126,234,0.08)" stroke-width="0.5"/></pattern></defs><rect width="100" height="100" fill="url(%23grid)"/></svg>');
opacity: 1;
pointer-events: none;
}
.hero .container {
max-width: 90rem;
margin: 0 auto;
padding: 0 2rem;
position: relative;
z-index: 2;
}
.hero-content {
max-width: 90rem;
margin: 0 auto;
position: relative;
z-index: 2;
}
.hero-badge {
display: inline-block;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.15), rgba(118, 75, 162, 0.15));
color: var(--primary);
padding: 1rem 2rem;
border-radius: 50px;
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 2rem;
backdrop-filter: blur(20px);
border: 1px solid rgba(102, 126, 234, 0.2);
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.1);
animation: fadeInUp 0.6s ease-out;
}
.hero-title {
font-size: clamp(3rem, 8vw, 6rem);
font-weight: 900;
line-height: 1.1;
margin-bottom: 2rem;
background: linear-gradient(135deg, #ffffff 0%, var(--primary) 50%, var(--secondary) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: fadeInUp 0.8s ease-out 0.2s both;
letter-spacing: -0.02em;
}
.hero-subtitle {
font-size: clamp(1.4rem, 3vw, 2rem);
font-weight: 400;
margin-bottom: 0;
opacity: 0.85;
max-width: 70rem;
margin-left: auto;
margin-right: auto;
color: var(--text-secondary);
animation: fadeInUp 1s ease-out 0.4s both;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ============================================
LIBRARY CONTENT - Organic Flow
============================================ */
.library-content,
.community-content {
background: linear-gradient(135deg, var(--bg-medium) 0%, var(--bg-light) 100%);
padding: 6rem 0;
border-radius: 50px 50px 0 0;
margin-top: -3rem;
position: relative;
z-index: 10;
min-height: 60vh;
}
.library-container,
.community-container {
max-width: 140rem;
margin: 0 auto;
padding: 0 2rem;
}
/* ============================================
STATS CARDS - Modern Grid
============================================ */
.stats-grid,
.community-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
margin-bottom: 4rem;
}
.stat-card {
background: var(--glass);
padding: 2.5rem;
border-radius: 24px;
backdrop-filter: blur(20px);
border: 1px solid var(--border);
text-align: center;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--primary), var(--secondary));
transform: scaleX(0);
transition: transform 0.4s ease;
}
.stat-card:hover::before {
transform: scaleX(1);
}
.stat-card:hover {
transform: translateY(-8px);
border-color: rgba(102, 126, 234, 0.4);
box-shadow: 0 20px 60px var(--shadow);
}
.stat-number {
font-size: clamp(2.5rem, 5vw, 4.8rem);
font-weight: 900;
color: var(--primary);
margin-bottom: 0.5rem;
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.stat-label {
color: var(--text-secondary);
font-size: 1.2rem;
font-weight: 500;
}
/* ============================================
FILTERS - Clean & Modern
============================================ */
.filters-section,
.unified-filters {
background: var(--glass);
padding: 2rem;
border-radius: 20px;
backdrop-filter: blur(20px);
border: 1px solid var(--border);
margin-bottom: 4rem;
}
.filter-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1.5rem;
align-items: end;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.filter-label {
font-size: 0.9rem;
color: var(--text-secondary);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.filter-select {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
border-radius: 12px;
color: var(--text-primary);
padding: 1rem 1.2rem;
font-size: 1rem;
transition: all 0.3s ease;
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
background-position: right 1rem center;
background-repeat: no-repeat;
background-size: 1.5rem;
padding-right: 3.5rem;
}
.filter-select:hover {
border-color: rgba(102, 126, 234, 0.4);
background-color: rgba(255, 255, 255, 0.08);
}
.filter-select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
}
/* ============================================
TRACKS GRID - Modern Feed Layout
============================================ */
.tracks-grid {
display: flex;
flex-direction: column;
gap: 2.5rem;
max-width: 90rem;
margin: 0 auto;
}
/* ============================================
MONTH GROUPING - Collapsible Sections
============================================ */
.month-group {
margin-bottom: 1rem;
}
.month-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.2rem 1.8rem;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.1) 100%);
border: 1px solid rgba(102, 126, 234, 0.25);
border-radius: 14px;
cursor: pointer;
transition: all 0.3s ease;
user-select: none;
margin-bottom: 0;
}
.month-header:hover {
background: linear-gradient(135deg, rgba(102, 126, 234, 0.22) 0%, rgba(118, 75, 162, 0.15) 100%);
border-color: rgba(102, 126, 234, 0.4);
transform: translateY(-1px);
}
.month-header-left {
display: flex;
align-items: center;
gap: 1rem;
}
.month-icon {
font-size: 1.4rem;
}
.month-title {
font-size: 1.3rem;
font-weight: 600;
color: #fff;
letter-spacing: 0.02em;
}
.month-count {
font-size: 0.95rem;
color: rgba(255, 255, 255, 0.6);
font-weight: 400;
margin-left: 0.5rem;
}
.month-toggle {
display: flex;
align-items: center;
gap: 0.8rem;
color: rgba(255, 255, 255, 0.5);
font-size: 0.9rem;
}
.month-toggle i {
font-size: 1.1rem;
transition: transform 0.3s ease;
}
.month-header.collapsed .month-toggle i {
transform: rotate(-90deg);
}
.month-tracks {
display: flex;
flex-direction: column;
gap: 1.5rem;
padding: 1.5rem 0 0.5rem 0;
overflow: hidden;
transition: max-height 0.4s ease, opacity 0.3s ease, padding 0.3s ease;
}
.month-tracks.collapsed {
max-height: 0 !important;
opacity: 0;
padding: 0;
pointer-events: none;
}
/* Stagger animation for tracks appearing */
.month-tracks .track-card-modern {
animation: fadeSlideIn 0.3s ease forwards;
opacity: 0;
}
@keyframes fadeSlideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.month-tracks .track-card-modern:nth-child(1) { animation-delay: 0.05s; }
.month-tracks .track-card-modern:nth-child(2) { animation-delay: 0.1s; }
.month-tracks .track-card-modern:nth-child(3) { animation-delay: 0.15s; }
.month-tracks .track-card-modern:nth-child(4) { animation-delay: 0.2s; }
.month-tracks .track-card-modern:nth-child(5) { animation-delay: 0.25s; }
/* ============================================
TRACK CARD - Next-Gen Design (Image-Based)
============================================ */
.track-card-modern {
background: var(--glass);
padding: 2rem;
border-radius: 20px;
backdrop-filter: blur(20px);
border: 1px solid var(--border);
transition: all 0.3s ease;
position: relative;
margin-bottom: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.track-card-modern:hover {
transform: translateY(-4px);
border-color: rgba(102, 126, 234, 0.4);
box-shadow: 0 20px 60px var(--shadow);
}
/* Status Badge (Top Right) */
.track-status-badge {
position: absolute;
top: 1.5rem;
right: 1.5rem;
background: linear-gradient(135deg, var(--success), #38a169);
color: white;
padding: 0.4rem 0.8rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
z-index: 10;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
pointer-events: none;
white-space: nowrap;
}
.track-status-badge.private {
background: linear-gradient(135deg, #9ca3af, #6b7280);
}
/* Track Card Content */
.track-card-content {
display: flex;
align-items: flex-start;
gap: 2rem;
}
/* Track Main Info (Icon + Details) */
.track-main-info {
display: flex;
gap: 1.5rem;
flex: 1;
min-width: 0;
}
/* Track Icon */
.track-icon {
width: 80px;
height: 80px;
min-width: 80px;
border-radius: 16px;
background: linear-gradient(135deg, var(--primary), var(--secondary));
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
color: white;
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3);
flex-shrink: 0;
position: relative;
cursor: pointer;
}
.track-icon:hover .track-image-upload-overlay {
opacity: 1;
}
/* Track Image Container */
.track-image-container {
width: 80px;
height: 80px;
min-width: 80px;
border-radius: 16px;
overflow: hidden;
flex-shrink: 0;
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3);
background: linear-gradient(135deg, var(--primary), var(--secondary));
position: relative;
cursor: pointer;
}
.track-image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: opacity 0.3s ease;
}
/* Upload Overlay - appears on hover */
.track-image-upload-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
opacity: 0;
transition: opacity 0.3s ease;
border-radius: 16px;
z-index: 5;
}
.track-image-container:hover .track-image-upload-overlay {
opacity: 1;
}
.track-image-container:hover .track-image {
opacity: 0.7;
}
.track-image-upload-icon,
.track-image-download-icon {
color: white;
font-size: 1.5rem;
pointer-events: none;
}
.track-image-action-button {
width: 36px;
height: 36px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
margin: 0;
}
.track-image-action-button:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
.track-image-action-button input[type="file"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
z-index: 10;
font-size: 0;
line-height: 0;
}
.track-image-upload-input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
z-index: 10;
}
.track-image-upload-overlay.uploading {
background: rgba(102, 126, 234, 0.8);
}
.track-image-upload-overlay.uploading .track-image-upload-icon {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Track Details */
.track-details {
flex: 1;
min-width: 0;
}
.track-name {
font-size: 1.5rem;
font-weight: 800;
color: var(--text-primary);
margin: 0 0 0.5rem 0;
line-height: 1.3;
}
.track-title-link {
color: var(--text-primary);
text-decoration: none;
transition: color 0.2s ease, opacity 0.2s ease;
}
.track-title-link:hover {
color: var(--primary-color);
opacity: 0.9;
}
.track-artist-name {
font-size: 1rem;
color: var(--text-secondary);
margin: 0 0 0.75rem 0;
}
.artist-name-link {
color: var(--text-secondary);
text-decoration: none;
transition: color 0.2s ease, opacity 0.2s ease;
}
.artist-name-link:hover {
color: var(--primary-color);
opacity: 0.9;
}
.track-description {
font-size: 0.9rem;
color: var(--text-secondary);
line-height: 1.5;
margin: 0 0 1rem 0;
opacity: 0.8;
}
/* Metadata Row */
.track-metadata-row {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
font-size: 0.85rem;
color: var(--text-secondary);
margin-top: 0.5rem;
}
.meta-item {
display: flex;
align-items: center;
gap: 0.3rem;
}
.meta-item strong {
color: var(--text-primary);
font-weight: 600;
}
/* Action Buttons Row */
.track-actions-row {
display: flex;
align-items: center;
gap: 0.75rem;
flex-shrink: 0;
}
/* Large Play Button */
.play-btn-large {
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary), var(--secondary));
border: none;
color: white;
font-size: 1.2rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
flex-shrink: 0;
}
.play-btn-large:hover {
transform: scale(1.1);
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.6);
}
/* Icon Action Buttons */
.action-icon-btn {
width: 40px;
height: 40px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
color: var(--text-secondary);
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.action-icon-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(102, 126, 234, 0.3);
color: var(--text-primary);
transform: translateY(-2px);
}
.action-icon-btn i.fa-heart:hover {
color: #ef4444;
}
.action-icon-btn.liked {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.3);
}
.action-icon-btn.liked i.fa-heart {
color: #ef4444;
}
.action-icon-btn.liked:hover i.fa-heart {
color: #dc2626;
}
/* Processing/Failed Indicators */
.processing-indicator,
.failed-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
border-radius: 10px;
font-size: 0.9rem;
font-weight: 600;
}
.processing-indicator {
background: rgba(245, 158, 11, 0.1);
color: var(--accent);
border: 1px solid rgba(245, 158, 11, 0.3);
}
/* Modern Processing Indicator */
.processing-indicator-modern {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.625rem 1rem;
background: linear-gradient(135deg, rgba(245, 158, 11, 0.1), rgba(245, 158, 11, 0.15));
border: 1.5px solid rgba(245, 158, 11, 0.25);
border-radius: 10px;
backdrop-filter: blur(8px);
box-shadow: 0 2px 10px rgba(245, 158, 11, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1);
min-width: 0;
flex: 1;
max-width: 100%;
position: relative;
z-index: 1;
margin-right: 0;
}
.processing-spinner {
flex-shrink: 0;
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
color: #f59e0b;
font-size: 0.95rem;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.processing-text {
display: flex;
flex-direction: column;
gap: 0.1rem;
min-width: 0;
flex: 1;
line-height: 1.3;
}
.processing-main {
font-size: 0.875rem;
font-weight: 600;
color: #f59e0b;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
letter-spacing: 0.01em;
}
.processing-subtitle {
font-size: 0.7rem;
font-weight: 400;
color: rgba(245, 158, 11, 0.65);
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
letter-spacing: 0.01em;
}
.failed-indicator {
background: rgba(239, 68, 68, 0.1);
color: var(--danger);
border: 1px solid rgba(239, 68, 68, 0.3);
}
.track-card::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.05), rgba(118, 75, 162, 0.05));
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
}
.track-card:hover {
transform: translateY(-8px);
border-color: rgba(102, 126, 234, 0.4);
box-shadow: 0 25px 80px var(--shadow);
}
.track-card:hover::before {
opacity: 1;
}
/* Track Header */
.track-header,
.compact-header {
padding: 2.5rem;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 2rem;
border-bottom: 1px solid var(--border);
}
.compact-header {
flex-wrap: wrap;
}
.artist-mini {
display: flex;
align-items: center;
gap: 1rem;
flex: 1;
min-width: 0;
}
.artist-avatar-mini {
flex-shrink: 0;
}
.default-avatar-mini {
width: 50px;
height: 50px;
border-radius: 12px;
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-size: 1.2rem;
}
.artist-info-mini {
flex: 1;
min-width: 0;
}
.track-title-mini {
font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: 800;
color: var(--text-primary);
margin-bottom: 0.3rem;
line-height: 1.2;
word-wrap: break-word;
}
.artist-name-mini {
font-size: 1.1rem;
color: var(--primary);
font-weight: 600;
}
.track-header-content {
flex: 1;
min-width: 0;
}
.track-title {
font-size: clamp(1.8rem, 4vw, 2.4rem);
font-weight: 800;
color: var(--text-primary);
margin-bottom: 0.5rem;
line-height: 1.2;
word-wrap: break-word;
}
.track-artist {
font-size: 1.3rem;
color: var(--primary);
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
.track-status {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.5rem;
}
.status-badge {
padding: 0.5rem 1rem;
border-radius: 12px;
font-size: 0.85rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-complete {
background: linear-gradient(135deg, var(--success), #38a169);
color: white;
}
.status-processing {
background: linear-gradient(135deg, var(--accent), #d97706);
color: white;
animation: pulse 2s infinite;
}
.status-failed {
background: linear-gradient(135deg, var(--danger), #dc2626);
color: white;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
/* Track Prompt */
.track-prompt {
padding: 2rem 2.5rem;
font-size: 1.3rem;
color: var(--text-secondary);
line-height: 1.7;
background: rgba(255, 255, 255, 0.02);
border-left: 4px solid var(--primary);
margin: 0;
}
/* Track Metadata */
.track-metadata {
padding: 2rem 2.5rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 1.5rem;
border-top: 1px solid var(--border);
background: rgba(255, 255, 255, 0.02);
}
.metadata-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.metadata-label {
font-size: 0.85rem;
color: var(--text-secondary);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.metadata-value {
font-size: 1.1rem;
color: var(--text-primary);
font-weight: 700;
}
/* Track Actions */
.track-actions {
padding: 2rem 2.5rem;
display: flex;
gap: 1rem;
flex-wrap: wrap;
border-top: 1px solid var(--border);
background: rgba(255, 255, 255, 0.02);
}
.action-btn {
flex: 1;
min-width: 140px;
padding: 1.2rem 2rem;
border-radius: 14px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.8rem;
border: none;
text-decoration: none;
}
.action-btn-primary {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3);
}
.action-btn-primary:hover {
transform: translateY(-3px);
box-shadow: 0 12px 32px rgba(102, 126, 234, 0.4);
}
.action-btn-secondary {
background: var(--glass);
color: var(--text-primary);
border: 1px solid var(--border);
}
.action-btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(102, 126, 234, 0.3);
transform: translateY(-2px);
}
.action-btn-compact {
padding: 0.8rem 1.5rem;
border-radius: 12px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
border: none;
text-decoration: none;
background: var(--glass);
color: var(--text-primary);
border: 1px solid var(--border);
}
.action-btn-compact:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(102, 126, 234, 0.3);
transform: translateY(-2px);
}
.action-btn-compact.primary {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
border: none;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.3);
}
.action-btn-compact.primary:hover {
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.4);
transform: translateY(-3px);
}
.track-number-badge {
position: absolute;
top: 1.5rem;
right: 1.5rem;
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
padding: 0.5rem 1rem;
border-radius: 12px;
font-size: 0.9rem;
font-weight: 800;
z-index: 10;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.3);
}
.track-badge {
display: inline-block;
background: linear-gradient(135deg, var(--accent), #d97706);
color: white;
padding: 0.4rem 0.8rem;
border-radius: 10px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
}
.error-message-modern {
padding: 1.5rem;
margin: 1.5rem 2.5rem;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 12px;
border-left: 4px solid var(--danger);
}
.error-header {
color: var(--danger);
font-weight: 700;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.error-content {
color: #fca5a5;
font-size: 0.95rem;
line-height: 1.6;
}
/* ============================================
RESPONSIVE - Mobile First
============================================ */
@media (max-width: 768px) {
.hero {
padding: 6rem 0 4rem;
}
.library-content,
.community-content {
padding: 4rem 0;
border-radius: 30px 30px 0 0;
}
.stats-grid,
.community-stats {
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
.filter-row {
grid-template-columns: 1fr;
}
.filter-group {
width: 100%;
}
#trackSearch {
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box;
}
.track-card-modern {
padding: 1.5rem;
}
.track-card-content {
flex-direction: column;
gap: 1.5rem;
}
.track-main-info {
flex-direction: column;
gap: 1rem;
}
.track-icon {
width: 60px;
height: 60px;
min-width: 60px;
font-size: 1.5rem;
}
.track-name {
font-size: 1.3rem;
}
.track-metadata-row {
flex-direction: column;
gap: 0.75rem;
}
.track-actions-row {
width: 100%;
justify-content: flex-start;
flex-wrap: wrap;
gap: 0.5rem;
}
.play-btn-large {
width: 48px;
height: 48px;
font-size: 1rem;
flex-shrink: 0;
}
.action-icon-btn {
width: 40px;
height: 40px;
font-size: 0.95rem;
flex-shrink: 0;
}
/* Variations grid mobile */
.variations-grid {
grid-template-columns: 1fr !important;
gap: 1rem;
padding: 1rem;
}
/* Track metadata mobile */
.track-metadata {
grid-template-columns: 1fr !important;
gap: 1rem;
}
/* Progress & Activity Section - Force single column on mobile */
div[style*="grid-template-columns: 1fr 1fr"] {
grid-template-columns: 1fr !important;
gap: 2rem !important;
}
/* My Crates Section - Mobile Responsive */
#cratesSection {
padding: 1.5rem !important;
margin-bottom: 2rem !important;
}
#cratesSection > div:first-child {
flex-direction: column !important;
align-items: flex-start !important;
gap: 1rem !important;
}
#cratesSection h3 {
font-size: 1.5rem !important;
}
#cratesSection button {
padding: 0.75rem 1.5rem !important;
font-size: 1rem !important;
width: 100%;
}
#cratesList {
grid-template-columns: 1fr !important;
gap: 1.5rem !important;
max-height: none !important;
}
}
@media (max-width: 480px) {
.hero {
padding: 4rem 0 3rem;
}
.library-content,
.community-content {
padding: 3rem 0;
}
.stats-grid,
.community-stats {
grid-template-columns: 1fr;
gap: 1rem;
}
.track-card-modern {
padding: 1.2rem;
}
.track-icon {
width: 50px;
height: 50px;
min-width: 50px;
font-size: 1.3rem;
}
.track-name {
font-size: 1.2rem;
}
/* Variations grid small mobile */
.variations-grid {
grid-template-columns: 1fr !important;
gap: 0.75rem;
padding: 0.75rem;
}
/* Track metadata small mobile */
.track-metadata {
grid-template-columns: 1fr !important;
gap: 0.75rem;
}
/* Progress & Activity Section - Force single column on small mobile */
div[style*="grid-template-columns: 1fr 1fr"] {
grid-template-columns: 1fr !important;
gap: 1.5rem !important;
}
/* My Crates Section - Small Mobile */
#cratesSection {
padding: 1.2rem !important;
}
#cratesSection h3 {
font-size: 1.3rem !important;
}
#cratesSection button {
padding: 0.65rem 1.2rem !important;
font-size: 0.95rem !important;
}
#cratesList {
gap: 1.2rem !important;
}
}
.play-btn-large {
width: 44px;
height: 44px;
font-size: 0.9rem;
flex-shrink: 0;
}
.action-icon-btn {
width: 38px;
height: 38px;
font-size: 0.9rem;
flex-shrink: 0;
}
.track-actions-row {
gap: 0.4rem;
}
}
/* ============================================
UTILITIES
============================================ */
.empty-state {
text-align: center;
padding: 6rem 2rem;
color: var(--text-secondary);
}
.empty-icon {
font-size: 5rem;
margin-bottom: 2rem;
opacity: 0.5;
}
.empty-title {
font-size: 2rem;
color: var(--text-primary);
margin-bottom: 1rem;
font-weight: 700;
}
.empty-description {
font-size: 1.2rem;
margin-bottom: 2rem;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
/* Alerts */
.alert {
padding: 1.5rem 2rem;
border-radius: 12px;
margin: 2rem auto;
max-width: 90rem;
backdrop-filter: blur(20px);
border: 1px solid;
}
.alert-success {
background: rgba(72, 187, 120, 0.1);
border-color: rgba(72, 187, 120, 0.3);
color: #48bb78;
}
.alert-error {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.3);
color: #ef4444;
}
/* ============================================
MODAL BASE STYLES
============================================ */
.modal {
display: none !important;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
z-index: 10000;
align-items: center;
justify-content: center;
padding: 2rem;
}
.modal-content {
background: linear-gradient(135deg, var(--bg-medium) 0%, var(--bg-light) 100%);
border: 1px solid var(--border);
border-radius: 24px;
max-width: 600px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
padding: 2rem;
backdrop-filter: blur(20px);
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
color: var(--text-primary);
}
/* Edit Modal Form Styles */
#editTrackModal .form-group {
margin-bottom: 1.5rem;
}
#editTrackModal .form-label {
display: block;
margin-bottom: 0.5rem;
color: var(--text-primary);
font-weight: 600;
font-size: 0.95rem;
}
#editTrackModal .form-input {
width: 100%;
padding: 0.75rem 1rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
color: var(--text-primary);
font-size: 0.95rem;
transition: all 0.3s ease;
box-sizing: border-box;
}
#editTrackModal .form-input:focus {
outline: none;
border-color: rgba(102, 126, 234, 0.5);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
background: rgba(255, 255, 255, 0.08);
}
#editTrackModal .form-input::placeholder {
color: rgba(255, 255, 255, 0.4);
}
#editTrackModal textarea.form-input {
resize: vertical;
min-height: 120px;
font-family: inherit;
}
#editTrackModal .form-label input[type="checkbox"] {
margin-right: 0.5rem;
width: auto;
cursor: pointer;
}
#editTrackModal .form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
#editTrackModal .btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 10px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
#editTrackModal .btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
#editTrackModal .btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
#editTrackModal .btn:not(.btn-primary) {
background: rgba(255, 255, 255, 0.1);
color: var(--text-primary);
border: 1px solid rgba(255, 255, 255, 0.2);
}
#editTrackModal .btn:not(.btn-primary):hover {
background: rgba(255, 255, 255, 0.15);
}
#editTrackModal h2 {
color: var(--text-primary);
margin: 0;
font-size: 1.5rem;
font-weight: 700;
}
#editTrackModal .btn i {
font-size: 0.9rem;
}
/* ============================================
VARIATIONS MODAL
============================================ */
.variations-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
z-index: 10000;
display: none;
align-items: center;
justify-content: center;
padding: 2rem;
}
.variations-modal.active {
display: flex;
}
/* ============================================
LYRICS MODAL
============================================ */
.lyrics-modal {
display: none !important;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
z-index: 10000;
align-items: center;
justify-content: center;
padding: 2rem;
}
.lyrics-modal.active {
display: flex !important;
}
/* ============================================
DOWNLOAD MODAL
============================================ */
.download-modal {
display: none !important;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
z-index: 10000;
align-items: center;
justify-content: center;
padding: 2rem;
}
.download-modal.active {
display: flex !important;
}
.variations-content {
background: linear-gradient(135deg, var(--bg-medium) 0%, var(--bg-light) 100%);
border: 1px solid var(--border);
border-radius: 24px;
max-width: 90rem;
max-height: 90vh;
width: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
backdrop-filter: blur(20px);
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5);
}
.variations-header {
background: linear-gradient(135deg, var(--primary), var(--secondary));
padding: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.variations-title {
color: white;
font-size: 1.8rem;
font-weight: 800;
margin: 0;
}
.close-variations {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
font-size: 1.2rem;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.close-variations:hover {
background: rgba(255, 255, 255, 0.3);
transform: rotate(90deg);
}
.variations-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
padding: 2rem;
overflow-y: auto;
flex: 1;
}
.variation-card {
background: var(--glass);
border: 1px solid var(--border);
border-radius: 16px;
padding: 1.5rem;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.variation-card:hover {
transform: translateY(-4px);
border-color: rgba(102, 126, 234, 0.4);
box-shadow: 0 12px 32px var(--shadow);
}
.variation-card.selected {
border-color: var(--primary);
background: rgba(102, 126, 234, 0.1);
box-shadow: 0 0 0 2px var(--primary);
}
.variation-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.variation-title {
font-weight: 700;
color: var(--text-primary);
font-size: 1.1rem;
}
.variation-index {
background: var(--primary);
color: white;
padding: 0.3rem 0.6rem;
border-radius: 8px;
font-size: 0.75rem;
font-weight: 700;
}
.variation-duration {
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 1rem;
}
.variation-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.variation-btn {
background: var(--glass);
border: 1px solid var(--border);
color: var(--text-primary);
padding: 0.6rem 1rem;
border-radius: 10px;
cursor: pointer;
font-size: 0.85rem;
font-weight: 600;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
.variation-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(102, 126, 234, 0.3);
}
.variation-btn.play {
background: linear-gradient(135deg, var(--primary), var(--secondary));
border: none;
color: white;
}
.variation-btn.download {
background: linear-gradient(135deg, var(--success), #38a169);
border: none;
color: white;
}
.variation-btn.select {
background: linear-gradient(135deg, var(--accent), #d97706);
border: none;
color: white;
}
.variation-btn.select.selected {
background: linear-gradient(135deg, var(--success), #38a169);
}
.variations-footer {
padding: 2rem;
border-top: 1px solid var(--border);
background: rgba(255, 255, 255, 0.02);
}
.variations-info {
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 1.5rem;
display: flex;
align-items: flex-start;
gap: 0.5rem;
line-height: 1.6;
}
.variations-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
}
.variations-btn {
padding: 0.9rem 2rem;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
border: none;
font-size: 1rem;
}
.variations-btn.cancel {
background: var(--glass);
color: var(--text-primary);
border: 1px solid var(--border);
}
.variations-btn.cancel:hover {
background: rgba(255, 255, 255, 0.1);
}
.variations-btn.save {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
}
.variations-btn.save:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.4);
}
.variations-btn.save:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
</style>
<!-- Hero Section -->
<section class="hero">
<div class="container">
<div class="hero-content">
<div class="hero-badge">🎵 <?= t('library.hero_badge') ?></div>
<h1 class="hero-title"><?= t('library.hero_title') ?></h1>
<p class="hero-subtitle"><?= t('library.hero_subtitle') ?></p>
</div>
</div>
</section>
<!-- Success/Error Messages -->
<?php if (isset($_SESSION['success_message'])): ?>
<div class="alert alert-success" style="margin: 1rem; padding: 1rem; background: #d4edda; color: #155724; border: 1px solid #c3e6cb; border-radius: 8px;">
<?= htmlspecialchars($_SESSION['success_message']) ?>
</div>
<?php unset($_SESSION['success_message']); ?>
<?php endif; ?>
<?php if (isset($_SESSION['error_message'])): ?>
<div class="alert alert-error" style="margin: 1rem; padding: 1rem; background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; border-radius: 8px;">
<?= htmlspecialchars($_SESSION['error_message']) ?>
</div>
<?php unset($_SESSION['error_message']); ?>
<?php endif; ?>
<!-- Community Content -->
<section class="community-content">
<div class="community-container">
<!-- Quick Actions -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem; margin-bottom: 4rem;">
<a href="/#create" style="background: rgba(255, 255, 255, 0.05); padding: 3rem; border-radius: 20px; backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1); transition: all 0.3s ease; text-align: center; text-decoration: none; color: white;">
<div style="font-size: 3rem; color: #667eea; margin-bottom: 1.5rem;">🎵</div>
<div style="font-size: 2rem; font-weight: 700; margin-bottom: 1rem; color: white;"><?= t('library.create_music') ?></div>
<div style="color: #a0aec0; font-size: 1.4rem; margin-bottom: 1.5rem; line-height: 1.5;"><?= t('library.create_music_desc') ?></div>
<div style="background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; padding: 1rem 2rem; border-radius: 12px; font-weight: 600; font-size: 1.3rem; cursor: pointer; transition: all 0.3s ease; text-decoration: none; display: inline-flex; align-items: center; gap: 0.8rem;">
<i class="fas fa-plus"></i> <?= t('btn.start_creating') ?>
</div>
</a>
<a href="/artists.php" style="background: rgba(255, 255, 255, 0.05); padding: 3rem; border-radius: 20px; backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1); transition: all 0.3s ease; text-align: center; text-decoration: none; color: white;">
<div style="font-size: 3rem; color: #667eea; margin-bottom: 1.5rem;">👥</div>
<div style="font-size: 2rem; font-weight: 700; margin-bottom: 1rem; color: white;"><?= t('library.discover_artists') ?></div>
<div style="color: #a0aec0; font-size: 1.4rem; margin-bottom: 1.5rem; line-height: 1.5;"><?= t('library.discover_artists_desc') ?></div>
<div style="background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; padding: 1rem 2rem; border-radius: 12px; font-weight: 600; font-size: 1.3rem; cursor: pointer; transition: all 0.3s ease; text-decoration: none; display: inline-flex; align-items: center; gap: 0.8rem;">
<i class="fas fa-users"></i> <?= t('library.browse_artists') ?>
</div>
</a>
</div>
<!-- Library Stats -->
<div class="community-stats">
<div class="stat-card">
<div class="stat-number"><?= $user_stats['total_tracks'] ?></div>
<div class="stat-label"><?= t('library.total_tracks') ?></div>
<div style="font-size: 1.2rem; margin-top: 0.5rem; color: #10b981;">+<?= $user_stats['completed_tracks'] ?> <?= t('library.completed') ?></div>
</div>
<div class="stat-card">
<div class="stat-number"><?= round(($user_stats['total_duration'] ?? 0) / 60, 1) ?></div>
<div class="stat-label"><?= t('library.total_minutes') ?></div>
<div style="font-size: 1.2rem; margin-top: 0.5rem; color: #a0aec0;"><?= $user_stats['total_duration'] ?? 0 ?> <?= t('library.seconds') ?></div>
</div>
<div class="stat-card">
<div class="stat-number"><?= $credits ?></div>
<div class="stat-label"><?= t('library.credits_left') ?></div>
<div style="font-size: 1.2rem; margin-top: 0.5rem; color: #a0aec0;">
<?php if ($subscription_info && $subscription_usage): ?>
<?= ucfirst($subscription_info['plan_name']) ?> <?= t('library.plan_label') ?> •
<?= ($subscription_usage['track_limit'] - ($subscription_usage['tracks_created'] ?? 0)) ?> <?= t('library.subscription_tracks_left') ?>
<?php else: ?>
<?= ucfirst($plan) ?> <?= t('library.plan') ?>
<?php endif; ?>
</div>
</div>
<div class="stat-card">
<div class="stat-number"><?= $user_level ?></div>
<div class="stat-label"><?= t('library.creator_level') ?></div>
<div style="font-size: 1.2rem; margin-top: 0.5rem; color: #10b981;"><?= $xp ?> XP</div>
</div>
</div>
<!-- Progress & Activity Section -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 3rem; margin-bottom: 4rem;">
<!-- Progress Overview -->
<div style="background: rgba(255, 255, 255, 0.05); padding: 2.5rem; border-radius: 20px; backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1);">
<h3 style="font-size: 2rem; font-weight: 700; color: white; margin-bottom: 2rem;"><?= t('library.progress_overview') ?></h3>
<div style="margin-bottom: 2rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<span style="font-size: 1.4rem; font-weight: 600; color: white;"><?= t('library.completion_rate') ?></span>
<span style="font-size: 1.4rem; color: #a0aec0;"><?php echo $user_stats['total_tracks'] > 0 ? round(($user_stats['completed_tracks'] / $user_stats['total_tracks']) * 100) : 0; ?>%</span>
</div>
<div style="width: 100%; height: 8px; background: rgba(255, 255, 255, 0.1); border-radius: 4px; overflow: hidden;">
<div style="height: 100%; background: linear-gradient(90deg, #667eea, #764ba2); border-radius: 4px; transition: width 0.3s ease; width: <?php echo $user_stats['total_tracks'] > 0 ? ($user_stats['completed_tracks'] / $user_stats['total_tracks']) * 100 : 0; ?>%;"></div>
</div>
</div>
<div style="margin-bottom: 2rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<span style="font-size: 1.4rem; font-weight: 600; color: white;"><?= t('library.next_level') ?></span>
<span style="font-size: 1.4rem; color: #a0aec0;"><?php echo $user_level < 5 ? $user_level + 1 : t('library.max'); ?></span>
</div>
<div style="width: 100%; height: 8px; background: rgba(255, 255, 255, 0.1); border-radius: 4px; overflow: hidden;">
<?php
$next_level_xp = $user_level < 5 ? ($user_level * 200) : 1000;
$progress = min(100, ($xp / $next_level_xp) * 100);
?>
<div style="height: 100%; background: linear-gradient(90deg, #667eea, #764ba2); border-radius: 4px; transition: width 0.3s ease; width: <?php echo $progress; ?>%;"></div>
</div>
</div>
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<span style="font-size: 1.4rem; font-weight: 600; color: white;"><?= t('library.credits_usage') ?></span>
<span style="font-size: 1.4rem; color: #a0aec0;"><?php echo $credits; ?> <?= t('library.remaining') ?></span>
</div>
<div style="width: 100%; height: 8px; background: rgba(255, 255, 255, 0.1); border-radius: 4px; overflow: hidden;">
<div style="height: 100%; background: linear-gradient(90deg, #667eea, #764ba2); border-radius: 4px; transition: width 0.3s ease; width: <?php echo max(0, 100 - ($credits / 5) * 100); ?>%;"></div>
</div>
</div>
<?php if ($subscription_info && $subscription_usage): ?>
<div style="margin-top: 2rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<span style="font-size: 1.4rem; font-weight: 600; color: white;">
<i class="fas fa-crown" style="color: #48bb78; margin-right: 0.5rem;"></i>
<?= ucfirst($subscription_info['plan_name']) ?> <?= t('library.subscription_label') ?>
</span>
<span style="font-size: 1.4rem; color: #48bb78;">
<?= ($subscription_usage['track_limit'] - ($subscription_usage['tracks_created'] ?? 0)) ?> / <?= $subscription_usage['track_limit'] ?> <?= t('subscription.tracks_left') ?>
</span>
</div>
<div style="width: 100%; height: 8px; background: rgba(255, 255, 255, 0.1); border-radius: 4px; overflow: hidden;">
<div style="height: 100%; background: linear-gradient(90deg, #48bb78, #059669); border-radius: 4px; transition: width 0.3s ease; width: <?php echo min(100, (($subscription_usage['tracks_created'] ?? 0) / max(1, $subscription_usage['track_limit'])) * 100); ?>%;"></div>
</div>
<div style="font-size: 0.9rem; color: #a0aec0; margin-top: 0.5rem; display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
<i class="fas fa-calendar-alt"></i>
<span>
<?= t('subscription.resets_on') ?>
<strong style="color: #48bb78;">
<?php
if ($subscription_info['current_period_end']) {
$period_end = strtotime($subscription_info['current_period_end']);
$now = time();
$days_remaining = max(0, floor(($period_end - $now) / 86400));
$formatted_date = date('F j, Y', $period_end);
echo $formatted_date;
if ($days_remaining > 0) {
echo ' (' . $days_remaining . ' ' . ($days_remaining === 1 ? t('subscription.day_remaining') : t('subscription.days_remaining')) . ')';
}
} else {
echo t('subscription.your_next_billing_date');
}
?>
</strong>
</span>
</div>
</div>
<?php endif; ?>
</div>
<!-- Recent Activity -->
<div style="background: rgba(255, 255, 255, 0.05); padding: 2.5rem; border-radius: 20px; backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1);">
<h3 style="font-size: 2rem; font-weight: 700; color: white; margin-bottom: 2rem;"><?= t('library.recent_activity') ?></h3>
<?php if (empty($recent_activity)): ?>
<div style="text-align: center; padding: 2rem 0; color: #a0aec0;">
<div style="font-size: 4rem; margin-bottom: 1rem; opacity: 0.5;">📝</div>
<h4 style="font-size: 1.6rem; color: white; margin-bottom: 0.5rem;"><?= t('library.no_activity') ?></h4>
<p style="font-size: 1.2rem; color: #a0aec0;"><?= t('library.no_activity_desc') ?></p>
</div>
<?php else: ?>
<?php foreach (array_slice($recent_activity, 0, 5) as $activity): ?>
<div style="display: flex; align-items: center; gap: 1.5rem; padding: 1.5rem 0; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
<div style="width: 4rem; height: 4rem; border-radius: 50%; background: linear-gradient(135deg, #667eea, #764ba2); display: flex; align-items: center; justify-content: center; font-size: 1.6rem; color: white;">🎵</div>
<div style="flex: 1;">
<div style="font-size: 1.4rem; font-weight: 600; color: white; margin-bottom: 0.5rem;"><?php echo htmlspecialchars($activity['title']); ?></div>
<div style="font-size: 1.2rem; color: #a0aec0;"><?php echo date('M j, g:i A', strtotime($activity['created_at'])); ?></div>
</div>
<div style="padding: 0.5rem 1rem; border-radius: 20px; font-size: 1.2rem; font-weight: 600; background: <?php echo $activity['status'] === 'complete' ? 'rgba(16, 185, 129, 0.2)' : ($activity['status'] === 'processing' ? 'rgba(245, 158, 11, 0.2)' : 'rgba(239, 68, 68, 0.2)'); ?>; color: <?php echo $activity['status'] === 'complete' ? '#10b981' : ($activity['status'] === 'processing' ? '#f59e0b' : '#ef4444'); ?>;">
<?php echo ucfirst($activity['status']); ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<!-- Credit History -->
<?php if (!empty($credit_history)): ?>
<div style="background: rgba(255, 255, 255, 0.05); padding: 2.5rem; border-radius: 20px; backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1); margin-bottom: 4rem;">
<h3 style="font-size: 2rem; font-weight: 700; color: white; margin-bottom: 2rem;"><?= t('library.credit_history') ?></h3>
<?php foreach ($credit_history as $transaction): ?>
<div style="display: flex; justify-content: space-between; align-items: center; padding: 1rem 0; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
<div style="flex: 1;">
<div style="font-size: 1.4rem; font-weight: 600; color: white; margin-bottom: 0.5rem;"><?php echo htmlspecialchars($transaction['description'] ?? 'Credit transaction'); ?></div>
<div style="font-size: 1.2rem; color: #a0aec0;"><?php echo date('M j, g:i A', strtotime($transaction['created_at'])); ?></div>
</div>
<div style="font-size: 1.6rem; font-weight: 700; color: <?php echo $transaction['amount'] > 0 ? '#10b981' : '#ef4444'; ?>;">
<?php echo $transaction['amount'] > 0 ? '+' : ''; ?><?php echo $transaction['amount']; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- My Crates Section -->
<div id="cratesSection" style="background: rgba(255, 255, 255, 0.05); padding: 2.5rem; border-radius: 20px; backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1); margin-bottom: 4rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
<h3 style="font-size: 2rem; font-weight: 700; color: white; margin: 0;">🎧 <?= t('library.crates.title') ?></h3>
<button onclick="showCreateCrateModal()" style="background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; padding: 1rem 2rem; border-radius: 12px; font-weight: 600; font-size: 1.3rem; cursor: pointer; transition: all 0.3s ease;">
<i class="fas fa-plus"></i> <?= t('library.crates.create_new') ?>
</button>
</div>
<div id="cratesList" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 2rem; max-height: 600px; overflow-y: auto; padding-right: 0.5rem;">
<!-- Crates will be loaded here via JavaScript -->
<div style="text-align: center; padding: 2rem; color: #a0aec0;">
<i class="fas fa-spinner fa-spin" style="font-size: 2rem; margin-bottom: 1rem;"></i>
<p><?= t('library.crates.loading') ?></p>
</div>
</div>
</div>
<!-- Unified Filter Controls -->
<div class="unified-filters">
<!-- Search Bar -->
<div class="filter-row" style="margin-bottom: 1rem;">
<div class="filter-group" style="flex: 1; max-width: 100%;">
<label class="filter-label"><?= t('library.search_tracks') ?></label>
<input type="text"
name="search"
id="trackSearch"
class="filter-select"
placeholder="<?= t('library.search_placeholder') ?>"
value="<?= htmlspecialchars($_GET['search'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
style="width: 100%; padding: 0.75rem 1rem; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 10px; color: white; font-size: 0.95rem;">
</div>
</div>
<div class="filter-row">
<div class="filter-group">
<label class="filter-label"><?= t('library.status') ?></label>
<select name="status" class="filter-select">
<option value="all" <?= $status_filter === 'all' ? 'selected' : '' ?>><?= t('library.all_tracks') ?></option>
<option value="complete" <?= $status_filter === 'complete' ? 'selected' : '' ?>><?= t('library.completed_tracks') ?></option>
<option value="processing" <?= $status_filter === 'processing' ? 'selected' : '' ?>><?= t('library.processing_tracks') ?></option>
<option value="failed" <?= $status_filter === 'failed' ? 'selected' : '' ?>><?= t('library.failed_tracks') ?></option>
</select>
</div>
<div class="filter-group">
<label class="filter-label"><?= t('library.sort_by') ?></label>
<select name="sort" class="filter-select">
<option value="latest" <?= $sort_filter === 'latest' ? 'selected' : '' ?>><?= t('library.latest_first') ?></option>
<option value="oldest" <?= $sort_filter === 'oldest' ? 'selected' : '' ?>><?= t('library.oldest_first') ?></option>
<option value="popular" <?= $sort_filter === 'popular' ? 'selected' : '' ?>><?= t('library.most_popular') ?></option>
<option value="most-played" <?= $sort_filter === 'most-played' ? 'selected' : '' ?>><?= t('library.most_played') ?></option>
</select>
</div>
<div class="filter-group">
<label class="filter-label"><?= t('library.time') ?></label>
<select name="time" class="filter-select">
<option value="all" <?= $time_filter === 'all' ? 'selected' : '' ?>><?= t('library.all_time') ?></option>
<option value="today" <?= $time_filter === 'today' ? 'selected' : '' ?>><?= t('library.today') ?></option>
<option value="week" <?= $time_filter === 'week' ? 'selected' : '' ?>><?= t('library.this_week') ?></option>
<option value="month" <?= $time_filter === 'month' ? 'selected' : '' ?>><?= t('library.this_month') ?></option>
</select>
</div>
<div class="filter-group">
<label class="filter-label"><?= t('library.genre') ?></label>
<select name="genre" class="filter-select">
<option value=""><?= t('library.all_genres') ?> (<?= count($available_genres) ?>)</option>
<?php foreach ($available_genres as $genre): ?>
<option value="<?= htmlspecialchars($genre, ENT_QUOTES, 'UTF-8') ?>" <?= $genre_filter === $genre ? 'selected' : '' ?>>
<?= htmlspecialchars($genre, ENT_QUOTES, 'UTF-8') ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<!-- Analysis Status & Batch Actions -->
<?php
// Count tracks needing analysis for current user
$pdo = getDBConnection();
$analysisStmt = $pdo->prepare("
SELECT COUNT(*) as needing_analysis
FROM music_tracks
WHERE user_id = ?
AND status = 'complete'
AND audio_url IS NOT NULL
AND audio_url != ''
AND (
JSON_EXTRACT(metadata, '$.bpm') IS NULL
OR JSON_EXTRACT(metadata, '$.bpm') = ''
OR JSON_EXTRACT(metadata, '$.analysis_source') IS NULL
OR JSON_EXTRACT(metadata, '$.analysis_source') = ''
)
");
$analysisStmt->execute([$_SESSION['user_id']]);
$analysisStats = $analysisStmt->fetch(PDO::FETCH_ASSOC);
$needingAnalysis = (int)$analysisStats['needing_analysis'];
?>
<?php if ($needingAnalysis > 0): ?>
<div style="background: rgba(251, 191, 36, 0.1); border: 1px solid rgba(251, 191, 36, 0.3); border-radius: 12px; padding: 1.5rem; margin-bottom: 2rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
<div>
<h3 style="color: #fbbf24; margin: 0 0 0.5rem 0; font-size: 1.2rem;">
<i class="fas fa-exclamation-triangle"></i> Analysis Needed
</h3>
<p style="color: rgba(255, 255, 255, 0.8); margin: 0; font-size: 0.95rem;">
<?= $needingAnalysis ?> track<?= $needingAnalysis !== 1 ? 's' : '' ?> need<?= $needingAnalysis === 1 ? 's' : '' ?> BPM and key analysis
</p>
</div>
<button onclick="batchAnalyzeTracks()" class="btn-batch-analyze" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 8px; font-weight: 600; cursor: pointer; display: flex; align-items: center; gap: 0.5rem; transition: all 0.3s ease;">
<i class="fas fa-magic"></i> Analyze All Tracks
</button>
</div>
<?php endif; ?>
<!-- Tracks Grid -->
<div class="tracks-grid">
<?php if (empty($recent_tracks)): ?>
<div style="grid-column: 1 / -1; text-align: center; padding: 4rem;">
<h3><?= t('library.no_tracks_found') ?></h3>
<p><?= t('library.no_tracks_desc') ?></p>
</div>
<?php else: ?>
<!-- Library Tracks Grid -->
<div class="tracks-grid">
<?php if (empty($tracks_with_variations)): ?>
<div class="empty-state">
<div class="empty-icon">🎵</div>
<?php if ($status_filter === 'processing'): ?>
<h2 class="empty-title"><?= t('library.no_processing') ?></h2>
<p class="empty-description">
<?= t('library.no_processing_desc') ?>
</p>
<a href="?status=all" class="create-first-btn">
<i class="fas fa-list"></i>
<?= t('library.view_all_tracks') ?>
</a>
<?php elseif ($status_filter === 'failed'): ?>
<h2 class="empty-title"><?= t('library.no_failed') ?></h2>
<p class="empty-description">
<?= t('library.no_failed_desc') ?>
</p>
<a href="?status=all" class="create-first-btn">
<i class="fas fa-list"></i>
<?= t('library.view_all_tracks') ?>
</a>
<?php elseif ($status_filter === 'complete'): ?>
<h2 class="empty-title"><?= t('library.no_completed') ?></h2>
<p class="empty-description">
<?= t('library.no_completed_desc') ?>
</p>
<a href="/#create" class="create-first-btn">
<i class="fas fa-plus"></i>
<?= t('library.create_first_track') ?>
</a>
<?php else: ?>
<h2 class="empty-title"><?= t('library.no_tracks_yet') ?></h2>
<p class="empty-description">
<?= t('library.no_tracks_yet_desc') ?>
</p>
<a href="/#create" class="create-first-btn">
<i class="fas fa-plus"></i>
<?= t('library.create_first_track') ?>
</a>
<?php endif; ?>
</div>
<?php else: ?>
<?php
// Group tracks by month for better organization
$tracks_by_month = [];
foreach ($tracks_with_variations as $track) {
$month_key = date('Y-m', strtotime($track['created_at']));
if (!isset($tracks_by_month[$month_key])) {
$tracks_by_month[$month_key] = [];
}
$tracks_by_month[$month_key][] = $track;
}
// Count total tracks first to get the correct numbering
$total_tracks = count($tracks_with_variations);
$track_number = $total_tracks; // Start from the oldest track (highest number)
// Iterate through months
foreach ($tracks_by_month as $month_key => $month_tracks):
$month_date = DateTime::createFromFormat('Y-m', $month_key);
$month_display = $month_date ? $month_date->format('F Y') : $month_key;
$track_count = count($month_tracks);
?>
<div class="month-group">
<div class="month-header" onclick="toggleMonthGroup(this)">
<div class="month-header-left">
<span class="month-icon">📅</span>
<span class="month-title"><?= htmlspecialchars($month_display, ENT_QUOTES, 'UTF-8') ?></span>
<span class="month-count">(<?= $track_count ?> <?= $track_count === 1 ? 'track' : 'tracks' ?>)</span>
</div>
<div class="month-toggle">
<i class="fas fa-chevron-down"></i>
</div>
</div>
<div class="month-tracks">
<?php foreach ($month_tracks as $track):
$displayTitle = $track['title'] ?: 'Untitled Track';
// Get selected variation index from metadata first (needed for duration calculation)
$trackMetadataForDuration = json_decode($track['metadata'] ?? '{}', true) ?: [];
$selectedVariationIndexForDuration = $trackMetadataForDuration['selected_variation'] ?? $track['selected_variation_index'] ?? 0;
// Get the selected variation to use its duration if available
$selectedVariationForDuration = null;
if (!empty($track['variations']) && is_array($track['variations'])) {
foreach ($track['variations'] as $variation) {
if (isset($variation['variation_index']) && $variation['variation_index'] == $selectedVariationIndexForDuration) {
$selectedVariationForDuration = $variation;
break;
}
}
if (!$selectedVariationForDuration && isset($track['variations'][$selectedVariationIndexForDuration])) {
$selectedVariationForDuration = $track['variations'][$selectedVariationIndexForDuration];
}
}
// Use selected variation's duration if available, otherwise use track's duration
$duration_seconds = $track['duration'] ?? 0;
if ($selectedVariationForDuration && !empty($selectedVariationForDuration['duration'])) {
$duration_seconds = $selectedVariationForDuration['duration'];
}
// Format duration as mm:ss
$duration_minutes = floor($duration_seconds / 60);
$duration_secs = $duration_seconds % 60;
$duration = sprintf('%d:%02d', $duration_minutes, $duration_secs);
$created_date = date('M j, Y', strtotime($track['created_at']));
// Parse metadata - only use actual values, no defaults
$metadata = json_decode($track['metadata'] ?? '{}', true) ?: [];
// Extract error message from metadata if track is failed
$error_message = null;
if ($track['status'] === 'failed' && !empty($metadata)) {
// Try multiple locations for error message
$raw_message = $metadata['msg'] ??
$metadata['error'] ??
$metadata['error_msg'] ??
$metadata['message'] ??
null;
// Format error message for better display
if ($raw_message) {
// ONLY remove API.Box references - keep the exact error message
$raw_message = preg_replace('/\b(API\.Box|api\.box|API\.box|not found on API\.Box|not found in API\.Box|Task not found in API\.Box|Track.*not found on API\.Box)\b/i', '', $raw_message);
$raw_message = trim($raw_message);
// Clean up multiple spaces that might result from removal
$raw_message = preg_replace('/\s+/', ' ', $raw_message);
// Check if this is actually a success message (not an error)
$is_success_message = preg_match('/\b(successfully|success|complete|done|ready|finished)\b/i', $raw_message);
// Only treat as error if it's NOT a success message
if (!$is_success_message && !empty($raw_message)) {
$error_message = $raw_message;
}
// If it's a success message, leave $error_message as null (don't show anything)
}
// If no error message found and we have a failed track, use default
// But only if we didn't find a success message (which would leave $error_message as null)
if ($error_message === null && empty($raw_message ?? null)) {
$error_message = "Track generation failed. Please try again.";
}
}
$genre = $metadata['genre'] ?? null;
// BPM: Use analyzed value if available, otherwise fallback to default
// SINGLE SOURCE OF TRUTH: Read from main fields only
$hasAnalyzedBPM = isset($metadata['bpm']) && isset($metadata['analysis_source']);
$rawBpm = $metadata['bpm'] ?? null;
// CLEAN: Only normalize clearly wrong values (188 -> 94), preserve valid BPMs
$bpm = $rawBpm;
if ($bpm) {
// 188 is a common error (94*2) - fix it specifically
if (abs($bpm - 188) < 2) {
$bpm = 94;
}
// Values > 200: Almost always wrong
elseif ($bpm > 200) {
$maxIterations = 10;
$iterations = 0;
while ($bpm > 200 && $iterations < $maxIterations) {
$bpm = $bpm / 2;
$iterations++;
}
if ($bpm > 200) $bpm = 200; // Final clamp
}
// Values < 50: Very low, likely wrong
elseif ($bpm < 50 && $bpm > 0) {
$doubled = $bpm * 2;
if ($doubled <= 200) {
$bpm = $doubled;
}
}
// Otherwise keep the original (60-200 is valid)
}
// KEY: Use analyzed value if available (same priority as track.php)
// SINGLE SOURCE OF TRUTH: Read from main fields only
$hasAnalyzedKey = isset($metadata['key']) && isset($metadata['analysis_source']);
$key = $metadata['key'] ?? null;
// CAMELOT/NUMERICAL KEY: Read from main field
$numericalKey = $metadata['numerical_key'] ?? '';
$mood = $metadata['mood'] ?? null;
// Only try to extract from prompt if we don't have analyzed data
if (empty($numericalKey) && !isset($metadata['analyzed_camelot']) && !empty($track['prompt'])) {
// Try to extract from prompt if not in metadata
if (preg_match('/key[:\s]+([0-9]+[A-G]?)\s*[–\-]?\s*/i', $track['prompt'], $keyMatches)) {
$numericalKey = trim($keyMatches[1]);
}
}
?>
<?php
// Get selected variation index from metadata
$trackMetadataForAttr = json_decode($track['metadata'] ?? '{}', true) ?: [];
$selectedVariationIndexForAttr = $trackMetadataForAttr['selected_variation'] ?? $track['selected_variation_index'] ?? 0;
// Handle track image - same logic as community_fixed.php
$imageUrl = $track['image_url'] ?? null;
// Reject external URLs
if (!empty($imageUrl) && (strpos($imageUrl, 'http://') === 0 || strpos($imageUrl, 'https://') === 0)) {
$imageUrl = null;
}
// Normalize image URL format
if ($imageUrl && !preg_match('/^https?:\/\//', $imageUrl)) {
if (!str_starts_with($imageUrl, '/')) {
$imageUrl = '/' . ltrim($imageUrl, '/');
}
}
// If still empty, try metadata
if (empty($imageUrl) || $imageUrl === 'null' || $imageUrl === 'NULL') {
if (!empty($track['metadata'])) {
$metadata = is_string($track['metadata']) ? json_decode($track['metadata'], true) : $track['metadata'];
if (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, '/');
}
$imageUrl = $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, '/');
}
$imageUrl = $metaCoverUrl;
}
}
}
// Try to find image file by task_id pattern
if (empty($imageUrl) && !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);
$imageUrl = '/uploads/track_covers/' . basename($mostRecent);
}
}
}
// Fallback to default
if (empty($imageUrl)) {
$imageUrl = '/assets/images/default-track.jpg';
}
}
?>
<div class="track-card-modern" data-track-id="<?= $track['id'] ?>" data-status="<?= $track['status'] ?>" data-variations="<?= htmlspecialchars(json_encode($track['variations'] ?? []), ENT_QUOTES, 'UTF-8') ?>" data-selected-variation="<?= $selectedVariationIndexForAttr ?>" data-prompt="<?= htmlspecialchars($track['prompt'] ?? '', ENT_QUOTES, 'UTF-8') ?>" data-created-at="<?= htmlspecialchars($track['created_at'], ENT_QUOTES, 'UTF-8') ?>">
<!-- Status Badge (Top Right) -->
<?php if ($track['is_public'] ?? 0): ?>
<div class="track-status-badge"><?= t('library.card.public') ?></div>
<?php else: ?>
<div class="track-status-badge private"><?= t('library.card.private') ?></div>
<?php endif; ?>
<!-- Track Content -->
<div class="track-card-content">
<!-- Track Image/Icon & Info -->
<div class="track-main-info">
<!-- Track Image or Icon -->
<?php if ($imageUrl && $imageUrl !== '/assets/images/default-track.jpg'): ?>
<div class="track-image-container" data-track-id="<?= $track['id'] ?>" data-user-id="<?= $track['user_id'] ?>" data-image-url="<?= htmlspecialchars($imageUrl, ENT_QUOTES, 'UTF-8') ?>">
<img src="<?= htmlspecialchars($imageUrl, ENT_QUOTES, 'UTF-8') ?>" alt="<?= htmlspecialchars($displayTitle, ENT_QUOTES, 'UTF-8') ?>" class="track-image" loading="lazy">
<?php if ($track['user_id'] == $_SESSION['user_id']): ?>
<div class="track-image-upload-overlay">
<button type="button" class="track-image-action-button track-image-download-btn" data-image-url="<?= htmlspecialchars($imageUrl, ENT_QUOTES, 'UTF-8') ?>" data-track-title="<?= htmlspecialchars($displayTitle, ENT_QUOTES, 'UTF-8') ?>" title="<?= htmlspecialchars(t('library.image.download'), ENT_QUOTES, 'UTF-8') ?>">
<i class="fas fa-download track-image-download-icon"></i>
</button>
<label class="track-image-action-button" for="track-image-upload-<?= $track['id'] ?>" title="<?= htmlspecialchars(t('library.image.upload'), ENT_QUOTES, 'UTF-8') ?>">
<i class="fas fa-camera track-image-upload-icon"></i>
<input type="file" id="track-image-upload-<?= $track['id'] ?>" class="track-image-upload-input" accept="image/jpeg,image/jpg,image/png,image/gif,image/webp" data-track-id="<?= $track['id'] ?>">
</label>
</div>
<?php endif; ?>
</div>
<?php else: ?>
<div class="track-icon" data-track-id="<?= $track['id'] ?>" data-user-id="<?= $track['user_id'] ?>">
<i class="fas fa-music"></i>
<?php if ($track['user_id'] == $_SESSION['user_id']): ?>
<div class="track-image-upload-overlay">
<label class="track-image-action-button" for="track-image-upload-icon-<?= $track['id'] ?>" title="<?= htmlspecialchars(t('library.image.upload'), ENT_QUOTES, 'UTF-8') ?>">
<i class="fas fa-camera track-image-upload-icon"></i>
<input type="file" id="track-image-upload-icon-<?= $track['id'] ?>" class="track-image-upload-input" accept="image/jpeg,image/jpg,image/png,image/gif,image/webp" data-track-id="<?= $track['id'] ?>">
</label>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Track Details -->
<div class="track-details">
<h3 class="track-name"><a href="/track.php?id=<?= $track['id'] ?>" class="track-title-link" target="_blank" rel="noopener noreferrer"><?= htmlspecialchars($displayTitle, ENT_QUOTES, 'UTF-8') ?></a></h3>
<p class="track-artist-name"><?= t('library.card.by') ?> <a href="/artist_profile.php?id=<?= $track['user_id'] ?>" class="artist-name-link"><?= htmlspecialchars($user_name, ENT_QUOTES, 'UTF-8') ?></a></p>
<?php if ($track['prompt']): ?>
<p class="track-description" title="<?= htmlspecialchars($track['prompt'], ENT_QUOTES, 'UTF-8') ?>">
<strong><?= t('library.card.original_prompt') ?></strong> <?= htmlspecialchars($track['prompt'], ENT_QUOTES, 'UTF-8') ?>
</p>
<?php elseif ($track['description']): ?>
<p class="track-description"><?= htmlspecialchars($track['description'], ENT_QUOTES, 'UTF-8') ?></p>
<?php endif; ?>
<!-- Metadata Row - Only show values that exist in metadata -->
<div class="track-metadata-row">
<?php if (!empty($duration)): ?>
<span class="meta-item"><strong><?= t('library.card.duration') ?></strong> <?= $duration ?></span>
<?php endif; ?>
<?php if (!empty($bpm)): ?>
<span class="meta-item" id="bpm-meta-<?= $track['id'] ?>">
<strong><?= t('library.card.bpm') ?></strong> <span class="bpm-value-display"><?= $bpm ?></span> BPM
<?php if ($hasAnalyzedBPM): ?>
<i class="fas fa-check-circle" style="color: #4ade80; margin-left: 4px;" title="Analyzed"></i>
<?php else: ?>
<i class="fas fa-exclamation-circle" style="color: #fbbf24; margin-left: 4px;" title="Needs Analysis"></i>
<?php endif; ?>
<?php if ($track['user_id'] == $_SESSION['user_id'] && $bpm): ?>
<button class="bpm-edit-btn" onclick="openBPMCorrectionModal(<?= $track['id'] ?>, <?= $bpm ?>)" title="Edit BPM" style="background: none; border: none; color: #667eea; cursor: pointer; margin-left: 4px; padding: 2px 4px;">
<i class="fas fa-edit" style="font-size: 0.75rem;"></i>
</button>
<?php endif; ?>
</span>
<?php elseif ($track['status'] === 'complete' && $track['user_id'] == $_SESSION['user_id']): ?>
<span class="meta-item" style="color: #fbbf24;" id="bpm-meta-<?= $track['id'] ?>">
<strong>BPM:</strong> <span class="bpm-value-display" style="font-style: italic;">Not analyzed</span>
<i class="fas fa-exclamation-circle" style="margin-left: 4px;" title="Needs Analysis"></i>
<button class="analyze-inline-btn" onclick="analyzeSingleTrack(<?= $track['id'] ?>, '<?= htmlspecialchars($displayTitle, ENT_QUOTES) ?>')" title="Analyze BPM & Key" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; color: white; padding: 4px 8px; border-radius: 4px; font-size: 0.75rem; cursor: pointer; margin-left: 8px; display: inline-flex; align-items: center; gap: 4px;">
<i class="fas fa-search"></i> Analyze
</button>
</span>
<?php endif; ?>
<?php if (!empty($genre)): ?>
<span class="meta-item"><strong><?= t('library.card.genre') ?></strong> <?= htmlspecialchars($genre, ENT_QUOTES, 'UTF-8') ?></span>
<?php endif; ?>
<?php if (!empty($mood)): ?>
<span class="meta-item"><strong><?= t('library.card.mood') ?></strong> <?= htmlspecialchars($mood, ENT_QUOTES, 'UTF-8') ?></span>
<?php endif; ?>
<?php if (!empty($key)): ?>
<span class="meta-item" id="key-meta-<?= $track['id'] ?>">
<strong><?= !empty($numericalKey) ? t('library.card.musical_key') : t('library.card.key') ?></strong>
<span class="key-value-display"><?= htmlspecialchars($key, ENT_QUOTES, 'UTF-8') ?></span>
<?php if (!empty($numericalKey)): ?>
<span class="camelot-value-display">(<?= htmlspecialchars($numericalKey, ENT_QUOTES, 'UTF-8') ?>)</span>
<?php endif; ?>
<?php if ($hasAnalyzedKey): ?>
<i class="fas fa-check-circle" style="color: #4ade80; margin-left: 4px;" title="Analyzed"></i>
<?php else: ?>
<i class="fas fa-exclamation-circle" style="color: #fbbf24; margin-left: 4px;" title="Needs Analysis"></i>
<?php endif; ?>
<?php if ($track['user_id'] == $_SESSION['user_id'] && $key): ?>
<button class="key-edit-btn" onclick="openKeyCorrectionModal(<?= $track['id'] ?>, '<?= htmlspecialchars($key, ENT_QUOTES) ?>', '<?= htmlspecialchars($numericalKey ?? '', ENT_QUOTES) ?>')" title="Edit Key" style="background: none; border: none; color: #667eea; cursor: pointer; margin-left: 4px; padding: 2px 4px;">
<i class="fas fa-edit" style="font-size: 0.75rem;"></i>
</button>
<?php endif; ?>
</span>
<?php elseif ($track['status'] === 'complete' && $track['user_id'] == $_SESSION['user_id']): ?>
<span class="meta-item" style="color: #fbbf24;" id="key-meta-<?= $track['id'] ?>">
<strong>Key:</strong> <span class="key-value-display" style="font-style: italic;">Not analyzed</span>
<i class="fas fa-exclamation-circle" style="margin-left: 4px;" title="Needs Analysis"></i>
<?php if (empty($bpm)): ?>
<button class="analyze-inline-btn" onclick="analyzeSingleTrack(<?= $track['id'] ?>, '<?= htmlspecialchars($displayTitle, ENT_QUOTES) ?>')" title="Analyze BPM & Key" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; color: white; padding: 4px 8px; border-radius: 4px; font-size: 0.75rem; cursor: pointer; margin-left: 8px; display: inline-flex; align-items: center; gap: 4px;">
<i class="fas fa-search"></i> Analyze
</button>
<?php endif; ?>
</span>
<?php endif; ?>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="track-actions-row">
<?php if ($track['status'] === 'complete'): ?>
<?php
// Get the selected variation if it exists
$selectedVariation = null;
// Extract selected_variation_index from metadata (stored as 'selected_variation' in metadata JSON)
$trackMetadata = json_decode($track['metadata'] ?? '{}', true) ?: [];
$selectedVariationIndex = $trackMetadata['selected_variation'] ?? $track['selected_variation_index'] ?? 0;
// Get the selected variation from the variations array
if (!empty($track['variations']) && is_array($track['variations'])) {
// Find variation by variation_index (not array index)
foreach ($track['variations'] as $variation) {
if (isset($variation['variation_index']) && $variation['variation_index'] == $selectedVariationIndex) {
$selectedVariation = $variation;
break;
}
}
// Fallback: use array index if variation_index doesn't match
if (!$selectedVariation && isset($track['variations'][$selectedVariationIndex])) {
$selectedVariation = $track['variations'][$selectedVariationIndex];
}
}
// Use selected variation's audio URL if available, otherwise use track's audio URL
$audioUrl = $track['audio_url'];
if ($selectedVariation && !empty($selectedVariation['audio_url'])) {
$audioUrl = $selectedVariation['audio_url'];
}
// Ensure audio URL is not empty
if (empty($audioUrl)) {
$audioUrl = $track['audio_url'] ?? '';
}
// Use signed proxy URL to protect direct audio file paths
// CRITICAL: Pass user_id and session_id for token binding (new protection)
$user_id = $_SESSION['user_id'] ?? null;
$session_id = session_id();
$proxyAudioUrl = getSignedAudioUrl($track['id'], null, null, $user_id, $session_id);
?>
<button class="play-btn-large" onclick="playTrackFromButton('<?= htmlspecialchars($proxyAudioUrl) ?>', '<?= htmlspecialchars($displayTitle) ?>', '<?= htmlspecialchars($user_name) ?>')"
data-audio-url="<?= htmlspecialchars($proxyAudioUrl) ?>"
data-title="<?= htmlspecialchars($displayTitle) ?>"
data-artist="<?= htmlspecialchars($user_name) ?>"
data-track-id="<?= $track['id'] ?>"
title="<?= t('library.card.play_track') ?>">
<i class="fas fa-play"></i>
</button>
<?php if ($track['variation_count'] > 0): ?>
<button class="action-icon-btn" onclick="showVariations(<?= $track['id'] ?>)" title="<?= t('library.card.view_variations') ?> (<?= $track['variation_count'] ?>)">
<i class="fas fa-layer-group"></i>
</button>
<?php endif; ?>
<button class="action-icon-btn" onclick="shareTrack(<?= $track['id'] ?>)" title="<?= t('library.card.share') ?>">
<i class="fas fa-share-alt"></i>
</button>
<button class="action-icon-btn" onclick="showAddToCrateModal(<?= $track['id'] ?>)" title="Add to Crate">
<i class="fas fa-folder-plus"></i>
</button>
<?php
// Check if user is admin (master upload is admin-only)
$isAdmin = isset($_SESSION['is_admin']) && $_SESSION['is_admin'];
if ($isAdmin): ?>
<!-- Upload Mastered Version Button (Admin only) -->
<label class="action-icon-btn" for="mastered-upload-<?= $track['id'] ?>" title="Upload Mastered Version (Admin only - MP3)">
<i class="fas fa-upload"></i>
<input type="file" id="mastered-upload-<?= $track['id'] ?>" class="mastered-upload-input" accept="audio/mpeg,audio/mp3,.mp3" data-track-id="<?= $track['id'] ?>" style="display: none;">
</label>
<?php endif; ?>
<button class="action-icon-btn <?= (isset($track['user_liked']) && (int)$track['user_liked'] > 0) ? 'liked' : '' ?>"
data-liked="<?= (isset($track['user_liked']) && (int)$track['user_liked'] > 0) ? '1' : '0' ?>"
onclick="toggleLike(<?= $track['id'] ?>, this)" title="<?= t('library.card.like') ?>">
<i class="fas fa-heart"></i>
</button>
<?php
// Check if this track has stems (check metadata regardless of music_type)
// Stems can exist for any track type if stem separation was performed
$hasStems = false;
$stemCount = 0;
$trackMetadata = json_decode($track['metadata'] ?? '{}', true) ?: [];
// Check for stem_files array in metadata (stored by callback.php)
$stemFiles = $trackMetadata['stem_files'] ?? [];
if (empty($stemFiles)) {
// Fallback: check for stems array (legacy format)
$stems = $trackMetadata['stems'] ?? [];
$stemFiles = $stems;
}
// Also check stem_count if available
if (empty($stemFiles) && isset($trackMetadata['stem_count']) && $trackMetadata['stem_count'] > 0) {
// Track has stem_count but no stem_files array - might be processing
$stemCount = (int)$trackMetadata['stem_count'];
}
if (!empty($stemFiles) && is_array($stemFiles)) {
$hasStems = true;
$stemCount = count($stemFiles);
}
?>
<?php if ($hasStems && $stemCount > 0): ?>
<button class="action-icon-btn" onclick="showStemsModal(<?= $track['id'] ?>)" title="View Stems (<?= $stemCount ?> stems)">
<i class="fas fa-layer-group"></i>
</button>
<?php elseif ($track['status'] === 'complete'): ?>
<!-- Only show stem separation button for complete tracks -->
<button class="action-icon-btn" onclick="openStemSeparationForTrack(<?= $track['id'] ?>)" title="Separate into Stems">
<i class="fas fa-cut"></i>
</button>
<?php endif; ?>
<?php if ($track['user_id'] == $_SESSION['user_id'] && $track['status'] === 'complete' && (empty($bpm) || empty($key) || !$hasAnalyzedBPM || !$hasAnalyzedKey)): ?>
<button class="action-icon-btn" onclick="analyzeSingleTrack(<?= $track['id'] ?>, '<?= htmlspecialchars($displayTitle, ENT_QUOTES) ?>')" title="Analyze BPM & Key">
<i class="fas fa-search"></i>
</button>
<?php endif; ?>
<button class="action-icon-btn edit-track-btn"
data-track-id="<?= $track['id'] ?>"
data-track-title="<?= htmlspecialchars($displayTitle, ENT_QUOTES) ?>"
data-track-prompt="<?= htmlspecialchars($track['prompt'] ?? '', ENT_QUOTES) ?>"
data-track-price="<?= $track['price'] ?? 0 ?>"
data-track-public="<?= $track['is_public'] ?? 0 ?>"
title="<?= t('library.card.edit_track') ?>">
<i class="fas fa-edit"></i>
</button>
<?php elseif ($track['status'] === 'processing' || !empty($track['_was_failed_but_checking'])): ?>
<div class="processing-indicator-modern">
<div class="processing-spinner">
<i class="fas fa-spinner fa-spin"></i>
</div>
<div class="processing-text">
<span class="processing-main"><?= t('library.card.processing') ?></span>
<span class="processing-subtitle"><?= !empty($track['_was_failed_but_checking']) ? 'Verifying status...' : t('library.card.processing_subtitle') ?></span>
</div>
</div>
<?php elseif ($track['status'] === 'failed'): ?>
<?php
// DOUBLE CHECK: Verify this track is actually failed, not still processing
// If it has a valid task_id and is recent, don't show Retry/Delete
$task_id = $track['task_id'] ?? $track['api_task_id'] ?? null;
$created_at = strtotime($track['created_at']);
$hours_old = (time() - $created_at) / 3600;
$might_still_be_processing = false;
if ($task_id &&
$task_id !== 'unknown' &&
!str_starts_with($task_id, 'temp_') &&
!str_starts_with($task_id, 'retry_') &&
$hours_old < 2) {
$might_still_be_processing = true;
}
?>
<?php if ($might_still_be_processing): ?>
<!-- Still might be processing - show Check Status only -->
<div class="processing-actions" style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<button class="action-btn secondary" onclick="checkTrackStatus(<?= $track['id'] ?>)" title="Check Status - Track may still be processing" style="padding: 0.5rem 1rem; background: #6b7280; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9rem;">
<i class="fas fa-sync-alt"></i>
Check Status
</button>
</div>
<?php else: ?>
<!-- Confirmed failed - show Retry/Delete -->
<div class="failed-actions-modern" style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<button class="action-btn primary" onclick="retryTrack(<?= $track['id'] ?>)" title="Create a new version with the same prompt" style="padding: 0.5rem 1rem; background: #667eea; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9rem;">
<i class="fas fa-redo"></i>
Retry
</button>
<button class="action-btn danger" onclick="deleteFailedTrack(<?= $track['id'] ?>)" title="Delete this failed track" style="padding: 0.5rem 1rem; background: #ef4444; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9rem;">
<i class="fas fa-trash"></i>
Delete
</button>
</div>
<?php endif; ?>
<?php else: ?>
<!-- Unknown status - treat as processing to be safe -->
<div class="processing-actions" style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<button class="action-btn secondary" onclick="checkTrackStatus(<?= $track['id'] ?>)" title="Check Status" style="padding: 0.5rem 1rem; background: #6b7280; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9rem;">
<i class="fas fa-sync-alt"></i>
Check Status
</button>
</div>
<?php endif; ?>
</div>
</div>
<!-- Error Message for Failed Tracks -->
<?php if ($track['status'] === 'failed' && $error_message): ?>
<div class="error-message-modern">
<div class="error-header">
<i class="fas fa-exclamation-triangle"></i>
<strong>Error Details:</strong>
</div>
<div class="error-content">
<?= htmlspecialchars($error_message) ?>
</div>
</div>
<?php endif; ?>
</div>
<?php $track_number--; // Decrement track number for next track (oldest to newest) ?>
<?php endforeach; // End tracks in month ?>
</div><!-- .month-tracks -->
</div><!-- .month-group -->
<?php endforeach; // End months ?>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<!-- Pagination Controls -->
<?php if ($total_pages > 1): ?>
<div class="pagination-container" style="margin-top: 3rem; padding: 2rem 0; border-top: 1px solid rgba(255, 255, 255, 0.1);">
<div style="display: flex; justify-content: center; align-items: center; gap: 1rem; flex-wrap: wrap;">
<?php
// Build query string for pagination links (preserve filters)
$query_params = $_GET;
unset($query_params['page']);
$query_string = !empty($query_params) ? '&' . http_build_query($query_params) : '';
?>
<!-- Previous Button -->
<?php if ($current_page > 1): ?>
<a href="?page=<?= $current_page - 1 ?><?= $query_string ?>"
class="pagination-btn"
style="padding: 0.75rem 1.5rem; background: rgba(102, 126, 234, 0.2); border: 1px solid rgba(102, 126, 234, 0.3); border-radius: 8px; color: #fff; text-decoration: none; transition: all 0.3s ease; display: inline-flex; align-items: center; gap: 0.5rem;">
<i class="fas fa-chevron-left"></i> Previous
</a>
<?php else: ?>
<span class="pagination-btn disabled" style="padding: 0.75rem 1.5rem; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; color: rgba(255, 255, 255, 0.3); display: inline-flex; align-items: center; gap: 0.5rem; cursor: not-allowed;">
<i class="fas fa-chevron-left"></i> Previous
</span>
<?php endif; ?>
<!-- Page Numbers -->
<div style="display: flex; gap: 0.5rem; align-items: center;">
<?php
$start_page = max(1, $current_page - 2);
$end_page = min($total_pages, $current_page + 2);
// Show first page if not in range
if ($start_page > 1): ?>
<a href="?page=1<?= $query_string ?>"
class="pagination-number"
style="padding: 0.75rem 1rem; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; color: #fff; text-decoration: none; min-width: 2.5rem; text-align: center; transition: all 0.3s ease;">
1
</a>
<?php if ($start_page > 2): ?>
<span style="color: rgba(255, 255, 255, 0.3);">...</span>
<?php endif; ?>
<?php endif; ?>
<?php for ($i = $start_page; $i <= $end_page; $i++): ?>
<?php if ($i == $current_page): ?>
<span class="pagination-number active"
style="padding: 0.75rem 1rem; background: linear-gradient(135deg, rgba(102, 126, 234, 0.3), rgba(118, 75, 162, 0.3)); border: 1px solid rgba(102, 126, 234, 0.5); border-radius: 8px; color: #fff; min-width: 2.5rem; text-align: center; font-weight: 600;">
<?= $i ?>
</span>
<?php else: ?>
<a href="?page=<?= $i ?><?= $query_string ?>"
class="pagination-number"
style="padding: 0.75rem 1rem; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; color: #fff; text-decoration: none; min-width: 2.5rem; text-align: center; transition: all 0.3s ease;">
<?= $i ?>
</a>
<?php endif; ?>
<?php endfor; ?>
<!-- Show last page if not in range -->
<?php if ($end_page < $total_pages): ?>
<?php if ($end_page < $total_pages - 1): ?>
<span style="color: rgba(255, 255, 255, 0.3);">...</span>
<?php endif; ?>
<a href="?page=<?= $total_pages ?><?= $query_string ?>"
class="pagination-number"
style="padding: 0.75rem 1rem; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; color: #fff; text-decoration: none; min-width: 2.5rem; text-align: center; transition: all 0.3s ease;">
<?= $total_pages ?>
</a>
<?php endif; ?>
</div>
<!-- Next Button -->
<?php if ($current_page < $total_pages): ?>
<a href="?page=<?= $current_page + 1 ?><?= $query_string ?>"
class="pagination-btn"
style="padding: 0.75rem 1.5rem; background: rgba(102, 126, 234, 0.2); border: 1px solid rgba(102, 126, 234, 0.3); border-radius: 8px; color: #fff; text-decoration: none; transition: all 0.3s ease; display: inline-flex; align-items: center; gap: 0.5rem;">
Next <i class="fas fa-chevron-right"></i>
</a>
<?php else: ?>
<span class="pagination-btn disabled" style="padding: 0.75rem 1.5rem; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; color: rgba(255, 255, 255, 0.3); display: inline-flex; align-items: center; gap: 0.5rem; cursor: not-allowed;">
Next <i class="fas fa-chevron-right"></i>
</span>
<?php endif; ?>
<!-- Page Info -->
<div style="margin-left: 1rem; color: rgba(255, 255, 255, 0.6); font-size: 0.9rem;">
Page <?= $current_page ?> of <?= $total_pages ?>
<span style="color: rgba(255, 255, 255, 0.4);">(<?= number_format($total_tracks) ?> tracks total)</span>
</div>
</div>
</div>
<?php endif; ?>
</div>
</section>
</div>
<script>
console.log('🎵 Community Fixed - JavaScript loading');
// Toggle month group collapse/expand
function toggleMonthGroup(header) {
const monthTracks = header.nextElementSibling;
const isCollapsed = header.classList.contains('collapsed');
if (isCollapsed) {
// Expand
header.classList.remove('collapsed');
monthTracks.classList.remove('collapsed');
// Set max-height for smooth animation
monthTracks.style.maxHeight = monthTracks.scrollHeight + 'px';
setTimeout(() => {
monthTracks.style.maxHeight = 'none';
}, 400);
} else {
// Collapse
monthTracks.style.maxHeight = monthTracks.scrollHeight + 'px';
// Force reflow
monthTracks.offsetHeight;
monthTracks.style.maxHeight = '0';
header.classList.add('collapsed');
monthTracks.classList.add('collapsed');
}
// Save state to localStorage
const monthKey = header.querySelector('.month-title').textContent;
const collapsedMonths = JSON.parse(localStorage.getItem('library_collapsed_months') || '{}');
collapsedMonths[monthKey] = !isCollapsed;
localStorage.setItem('library_collapsed_months', JSON.stringify(collapsedMonths));
}
// Restore collapsed state on page load
document.addEventListener('DOMContentLoaded', function() {
const collapsedMonths = JSON.parse(localStorage.getItem('library_collapsed_months') || '{}');
document.querySelectorAll('.month-header').forEach(header => {
const monthKey = header.querySelector('.month-title').textContent;
if (collapsedMonths[monthKey]) {
const monthTracks = header.nextElementSibling;
header.classList.add('collapsed');
monthTracks.classList.add('collapsed');
monthTracks.style.maxHeight = '0';
}
});
});
// Enhanced filter function with genre support
function filterTracks() {
const sortFilter = document.getElementById('sort-filter').value;
const timeFilter = document.getElementById('time-filter').value;
const genreFilter = document.getElementById('genre-filter').value;
const url = new URL(window.location);
url.searchParams.set('sort', sortFilter);
url.searchParams.set('time', timeFilter);
if (genreFilter) {
url.searchParams.set('genre', genreFilter);
} else {
url.searchParams.delete('genre');
}
// Reload page with new filters
window.location.search = url.search;
}
function filterByGenre(genre) {
const currentUrl = new URL(window.location);
currentUrl.searchParams.set('genre', genre);
currentUrl.searchParams.delete('page'); // Reset to page 1 when filtering
// Reload page with new filters
window.location.search = currentUrl.search;
}
// Working like function with API integration (matching community_fixed.php)
function toggleLike(trackId, button) {
console.log('❤️ Toggling like for track:', trackId);
// Prevent double-clicks
if (button.disabled) return;
button.disabled = true;
fetch('/api/toggle_like.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({track_id: trackId})
})
.then(r => {
if (!r.ok) {
throw new Error(`HTTP error! status: ${r.status}`);
}
return r.json();
})
.then(data => {
console.log('❤️ Like API response:', data);
if (data.success) {
// Update button state based on API response
if (data.liked) {
button.classList.add('liked');
} else {
button.classList.remove('liked');
}
// Update like count if displayed
const countSpan = button.querySelector('span');
if (countSpan) {
countSpan.textContent = data.like_count || 0;
}
console.log('❤️ Like toggled successfully. Liked:', data.liked);
} else {
console.error('❤️ Like failed:', data.error);
if (typeof showNotification === 'function') {
showNotification(data.error || 'Failed to like track', 'error');
}
}
button.disabled = false;
})
.catch(error => {
console.error('❤️ Like error:', error);
if (typeof showNotification === 'function') {
showNotification('Failed to like track. Please try again.', 'error');
}
button.disabled = false;
});
}
// Share track function - now shows modal with options
function shareTrack(trackId) {
console.log('🚀 Share button clicked for track:', trackId);
if (!trackId) {
console.error('🚀 No track ID provided');
if (typeof showNotification === 'function') {
showNotification('Invalid track ID', 'error');
}
return;
}
// Get track details from the page
let trackCard = document.querySelector(`[data-track-id="${trackId}"]`);
if (!trackCard) {
// Try alternative selector - find button and get parent card
const shareButton = document.querySelector(`[onclick*="shareTrack(${trackId})"]`);
if (shareButton) {
trackCard = shareButton.closest('.track-card, .library-item, [class*="track"], [class*="card"], .item');
}
}
// Get track title and artist name from the card, or use defaults
let trackTitle = 'Track';
let artistName = 'Artist';
if (trackCard) {
const trackNameElement = trackCard.querySelector('.track-name, [class*="title"], h3, h4');
if (trackNameElement) {
trackTitle = trackNameElement.textContent?.trim() || trackNameElement.querySelector('a')?.textContent?.trim() || 'Track';
}
const artistNameElement = trackCard.querySelector('.track-artist-name, [class*="artist"], [class*="by"]');
if (artistNameElement) {
artistName = artistNameElement.textContent?.replace(/^By\s+/i, '').trim() || artistNameElement.querySelector('a')?.textContent?.trim() || 'Artist';
}
} else {
console.warn('🚀 Track card not found, using default values');
}
// Check if track is public first
fetch('/api_track_management.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'generate_share_token',
track_id: trackId
})
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log('Share token response:', data);
if (data.success) {
// Show share modal with options
showShareOptionsModal(trackId, trackTitle, artistName, data.is_public === 1, data.share_url);
} else {
console.warn('Token generation failed:', data.message);
// If token generation fails, still show modal but with limited options
// Try to determine if track is private from the card or assume it might be
showShareOptionsModal(trackId, trackTitle, artistName, null, null);
}
})
.catch(error => {
console.error('Error checking track status:', error);
// Show modal anyway - assume it might be private to show warning
showShareOptionsModal(trackId, trackTitle, artistName, null, null);
});
}
// Show share options modal
function showShareOptionsModal(trackId, trackTitle, artistName, isPublic, shareUrl) {
// Create or get modal
let modal = document.getElementById('shareOptionsModal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'shareOptionsModal';
modal.className = 'modal';
document.body.appendChild(modal);
}
const isPrivate = isPublic === false;
const shareText = `🎵 Check out "${trackTitle}" by ${artistName} on SoundStudioPro!`;
modal.innerHTML = `
<div class="modal-content">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2>${libraryTranslations.share_title}</h2>
<button onclick="closeShareOptionsModal()" class="btn" style="background: transparent; border: none; font-size: 1.5rem; color: var(--text-primary); cursor: pointer;">
<i class="fas fa-times"></i>
</button>
</div>
${isPrivate ? `
<div style="background: rgba(255, 193, 7, 0.1); border: 1px solid rgba(255, 193, 7, 0.3); border-radius: 10px; padding: 1rem; margin-bottom: 1.5rem;">
<div style="display: flex; align-items: start; gap: 0.75rem;">
<i class="fas fa-info-circle" style="color: #ffc107; font-size: 1.2rem; margin-top: 0.2rem;"></i>
<div>
<strong style="color: #ffc107; display: block; margin-bottom: 0.5rem;">${libraryTranslations.share_important_notice}</strong>
<p style="margin: 0; color: var(--text-primary); font-size: 0.9rem; line-height: 1.5;">
${libraryTranslations.share_private_warning}
</p>
</div>
</div>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; color: var(--text-primary); font-weight: 600; font-size: 0.9rem;">
${libraryTranslations.share_expiration_label}
</label>
<select id="shareExpirationSelect" style="width: 100%; padding: 0.75rem; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 10px; color: var(--text-primary); font-size: 0.95rem;">
<option value="3600">${libraryTranslations.share_expiration_1hour}</option>
<option value="21600">${libraryTranslations.share_expiration_6hours}</option>
<option value="86400">${libraryTranslations.share_expiration_24hours}</option>
<option value="604800">${libraryTranslations.share_expiration_7days}</option>
<option value="2592000">${libraryTranslations.share_expiration_30days}</option>
</select>
</div>
` : ''}
<div style="display: flex; flex-direction: column; gap: 1rem;">
<button onclick="shareWithLink(${trackId}, '${trackTitle.replace(/'/g, "\\'")}', '${artistName.replace(/'/g, "\\'")}', ${isPrivate ? 'true' : 'false'})"
class="btn btn-primary" style="width: 100%; justify-content: center;">
<i class="fas fa-link"></i> ${libraryTranslations.share_share_with_link}
${isPrivate ? '<span style="font-size: 0.8rem; opacity: 0.9; margin-left: 0.5rem;">(' + libraryTranslations.share_accessible_via_link + ')</span>' : ''}
</button>
${isPrivate ? `
<button onclick="makePublicAndShare(${trackId}, '${trackTitle.replace(/'/g, "\\'")}', '${artistName.replace(/'/g, "\\'")}')"
class="btn" style="width: 100%; justify-content: center; background: rgba(76, 175, 80, 0.2); border: 1px solid rgba(76, 175, 80, 0.5); color: #4caf50;">
<i class="fas fa-globe"></i> ${libraryTranslations.share_make_public_and_share}
</button>
` : ''}
<button onclick="closeShareOptionsModal()"
class="btn" style="width: 100%; justify-content: center; background: rgba(255, 255, 255, 0.1);">
${libraryTranslations.share_cancel}
</button>
</div>
</div>
`;
// Use setProperty with important flag to override !important in CSS
modal.style.setProperty('display', 'flex', 'important');
document.body.style.overflow = 'hidden';
// Close on outside click
modal.onclick = function(event) {
if (event.target === modal) {
closeShareOptionsModal();
}
};
}
// Close share options modal
function closeShareOptionsModal() {
const modal = document.getElementById('shareOptionsModal');
if (modal) {
modal.style.setProperty('display', 'none', 'important');
document.body.style.overflow = '';
}
}
// Share with link (generates share token if needed)
function shareWithLink(trackId, trackTitle, artistName, isPrivate) {
closeShareOptionsModal();
// Get expiration time from select (if private track)
let expiresIn = 3600; // Default 1 hour
if (isPrivate) {
const expirationSelect = document.getElementById('shareExpirationSelect');
if (expirationSelect) {
expiresIn = parseInt(expirationSelect.value) || 3600;
}
}
// Generate or get share token
fetch('/api_track_management.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'generate_share_token',
track_id: trackId,
expires_in: expiresIn
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
const shareUrl = data.share_url;
const shareText = `🎵 Check out "${trackTitle}" by ${artistName} on SoundStudioPro!`;
// Record the share
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'share',
track_id: trackId,
platform: 'web'
})
}).catch(err => console.error('Share recording error:', err));
// Perform share action
if (navigator.share) {
navigator.share({
title: `${trackTitle} by ${artistName}`,
text: shareText,
url: shareUrl
})
.then(() => {
if (typeof showNotification === 'function') {
showNotification(libraryTranslations.share_success, 'success');
}
})
.catch(error => {
console.log('Web Share API cancelled, using clipboard:', error);
copyToClipboard(shareText, shareUrl);
});
} else {
copyToClipboard(shareText, shareUrl);
}
} else {
if (typeof showNotification === 'function') {
showNotification(libraryTranslations.share_failed, 'error');
}
}
})
.catch(error => {
console.error('Error generating share token:', error);
if (typeof showNotification === 'function') {
showNotification(libraryTranslations.share_failed, 'error');
}
});
}
// Make public and share
function makePublicAndShare(trackId, trackTitle, artistName) {
closeShareOptionsModal();
// First make track public
fetch('/api_track_management.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'toggle_visibility',
track_id: trackId,
visibility: 1
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Now share with regular URL (no token needed since it's public)
const trackUrl = `${window.location.origin}/track.php?id=${trackId}`;
const shareText = `🎵 Check out "${trackTitle}" by ${artistName} on SoundStudioPro!`;
// Record the share
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'share',
track_id: trackId,
platform: 'web'
})
}).catch(err => console.error('Share recording error:', err));
// Perform share action
if (navigator.share) {
navigator.share({
title: `${trackTitle} by ${artistName}`,
text: shareText,
url: trackUrl
})
.then(() => {
if (typeof showNotification === 'function') {
showNotification(libraryTranslations.share_make_public_success, 'success');
}
})
.catch(error => {
console.log('Web Share API cancelled, using clipboard:', error);
copyToClipboard(shareText, trackUrl);
});
} else {
copyToClipboard(shareText, trackUrl);
}
} else {
if (typeof showNotification === 'function') {
showNotification(libraryTranslations.share_make_public_failed, 'error');
}
}
})
.catch(error => {
console.error('Error making track public:', error);
if (typeof showNotification === 'function') {
showNotification(libraryTranslations.share_make_public_failed, 'error');
}
});
}
// Helper function to copy to clipboard (matching community_fixed.php pattern)
function copyToClipboard(text, url) {
const shareData = `${text}\n${url}`;
const isSecureContext = window.isSecureContext || location.protocol === 'https:' || location.hostname === 'localhost';
if (isSecureContext && navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(shareData).then(() => {
console.log('🚀 Link copied to clipboard');
if (typeof showNotification === 'function') {
showNotification('🔗 Track link copied to clipboard!', 'success');
} else {
alert('Track link copied to clipboard!');
}
}).catch(error => {
console.error('🚀 Clipboard API error:', error);
fallbackCopyToClipboard(shareData);
});
} else {
fallbackCopyToClipboard(shareData);
}
}
// Fallback copy using textarea (better mobile support)
function fallbackCopyToClipboard(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);
}
}
// Working comments function with modal
function showComments(trackId) {
// Create and show comments modal
const modal = document.createElement('div');
modal.className = 'comments-modal';
modal.innerHTML = `
<div class="comments-overlay">
<div class="comments-container">
<div class="comments-header">
<h3>Comments</h3>
<button class="close-btn" onclick="closeComments()">×</button>
</div>
<div class="comments-list" id="comments-list-${trackId}">
<div class="loading">Loading comments...</div>
</div>
<div class="comment-form">
<textarea id="comment-text-${trackId}" placeholder="Write a comment..." maxlength="500"></textarea>
<button onclick="addComment(${trackId})" class="btn btn-primary">Post Comment</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
// Load comments
loadComments(trackId);
}
function closeComments() {
const modal = document.querySelector('.comments-modal');
if (modal) {
modal.remove();
}
}
function loadComments(trackId) {
console.log('🎵 Loading comments for track:', trackId);
fetch(`/api_social.php?action=get_comments&track_id=${trackId}`)
.then(response => {
console.log('🎵 Load comments API response status:', response.status);
return response.json();
})
.then(data => {
console.log('🎵 Load comments API response data:', data);
const commentsList = document.getElementById(`comments-list-${trackId}`);
if (data.success && data.comments && data.comments.length > 0) {
commentsList.innerHTML = data.comments.map(comment => `
<div class="comment-item">
<div class="comment-avatar">
${comment.profile_image ?
`<img src="${comment.profile_image}" alt="${comment.user_name}" onerror="this.parentElement.innerHTML='<div class=\'default-avatar-small\'>${comment.user_name.charAt(0)}</div>'">` :
`<div class="default-avatar-small">${comment.user_name.charAt(0)}</div>`
}
</div>
<div class="comment-content">
<div class="comment-header">
<span class="comment-author">${comment.user_name}</span>
<span class="comment-time">${comment.created_at}</span>
</div>
<div class="comment-text">${comment.comment}</div>
</div>
</div>
`).join('');
} else {
commentsList.innerHTML = '<div class="no-comments">No comments yet. Be the first to comment!</div>';
}
})
.catch(error => {
console.warn('🎵 Load comments error:', error);
document.getElementById(`comments-list-${trackId}`).innerHTML = '<div class="error">Failed to load comments</div>';
});
}
function addComment(trackId) {
if (!<?= $user_id ? 'true' : 'false' ?>) {
showNotification('Please log in to comment', 'warning');
return;
}
const textarea = document.getElementById(`comment-text-${trackId}`);
const comment = textarea.value.trim();
if (!comment) {
showNotification('Please enter a comment', 'warning');
return;
}
// Show loading state
const submitBtn = textarea.nextElementSibling;
const originalText = submitBtn.textContent;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Posting...';
submitBtn.disabled = true;
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'comment',
track_id: trackId,
comment: comment
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
textarea.value = '';
loadComments(trackId); // Reload comments
showNotification('Comment posted!', 'success');
} else {
showNotification(data.message || 'Failed to post comment', 'error');
}
})
.catch(error => {
console.warn('🎵 Comment error:', error);
showNotification('Failed to post comment. Please try again.', 'error');
})
.finally(() => {
// Restore button
submitBtn.textContent = originalText;
submitBtn.disabled = false;
});
}
// Notification system
function showNotification(message, type = 'info') {
// Remove existing notifications
const existingNotifications = document.querySelectorAll('.notification');
existingNotifications.forEach(notification => notification.remove());
// Create notification element
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.innerHTML = `
<div class="notification-content">
<i class="notification-icon ${getNotificationIcon(type)}"></i>
<span class="notification-message">${message}</span>
<button class="notification-close" onclick="this.parentElement.parentElement.remove()">
<i class="fas fa-times"></i>
</button>
</div>
`;
// Add to page
document.body.appendChild(notification);
// Show animation
setTimeout(() => {
notification.classList.add('show');
}, 100);
// Auto remove after 5 seconds
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
if (notification.parentElement) {
notification.remove();
}
}, 300);
}, 5000);
}
function getNotificationIcon(type) {
switch (type) {
case 'success': return 'fas fa-check-circle';
case 'error': return 'fas fa-exclamation-circle';
case 'warning': return 'fas fa-exclamation-triangle';
default: return 'fas fa-info-circle';
}
}
// Enhanced global player initialization check
function waitForEnhancedPlayerCallback(callback, maxAttempts = 20) {
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
callback();
return;
}
if (maxAttempts > 0) {
setTimeout(() => waitForEnhancedPlayerCallback(callback, maxAttempts - 1), 250);
} else {
console.warn('⚠️ Enhanced global player not available, attempting to initialize...');
showNotification('Audio player not available. Please refresh the page.', 'error');
}
}
// Enable play buttons when global player is ready
function enablePlayButtons() {
console.log('🎵 Enabling play buttons - global player ready');
// Enable all play buttons
document.querySelectorAll('.action-btn.play-btn, .play-track-btn').forEach(btn => {
btn.classList.add('ready');
btn.disabled = false;
// Remove loading state if present
const icon = btn.querySelector('i');
if (icon && icon.className.includes('fa-spinner')) {
icon.className = 'fas fa-play';
}
});
// Show ready notification
showNotification('🎵 Audio player ready!', 'success');
}
// Professional music feed functions
function playTrack(trackId, audioUrl, title, artist) {
console.log('🎵 Playing track:', { trackId, audioUrl, title, artist });
// Ensure global player is ready before attempting playback
if (!window.enhancedGlobalPlayer || typeof window.enhancedGlobalPlayer.playTrack !== 'function') {
console.warn('❌ Global player not available');
showNotification('Audio player not ready. Please refresh the page.', 'error');
return;
}
// Ensure audio URL is absolute if it's relative
let finalAudioUrl = audioUrl;
if (audioUrl && !audioUrl.startsWith('http') && !audioUrl.startsWith('//')) {
if (audioUrl.startsWith('/')) {
finalAudioUrl = window.location.origin + audioUrl;
} else {
finalAudioUrl = window.location.origin + '/' + audioUrl;
}
console.log('🎵 Converted relative URL to absolute:', finalAudioUrl);
}
if (!finalAudioUrl || finalAudioUrl.trim() === '') {
console.error('❌ Audio URL is empty!');
showNotification('Audio file not available.', 'error');
return;
}
try {
// Call the global player's playTrack function
const success = window.enhancedGlobalPlayer.playTrack(finalAudioUrl, title, artist);
if (success) {
// Update UI - Mark this track as currently playing
document.querySelectorAll('.track-card').forEach(card => {
card.classList.remove('currently-playing', 'playing');
});
// Reset all play buttons
document.querySelectorAll('.action-btn.play-btn, .play-track-btn, .action-btn-compact.primary').forEach(btn => {
btn.classList.remove('playing');
const icon = btn.querySelector('i');
if (icon) {
icon.className = 'fas fa-play';
}
});
const currentCard = document.querySelector(`[data-track-id="${trackId}"]`);
if (currentCard) {
currentCard.classList.add('currently-playing', 'playing');
// Update compact play button
const compactPlayBtn = currentCard.querySelector('.action-btn.play-btn, .action-btn-compact.primary');
if (compactPlayBtn) {
compactPlayBtn.classList.add('playing');
const icon = compactPlayBtn.querySelector('i');
if (icon) icon.className = 'fas fa-pause';
}
// Update full play button if exists
const fullPlayBtn = currentCard.querySelector('.play-track-btn');
if (fullPlayBtn) {
fullPlayBtn.classList.add('playing');
fullPlayBtn.innerHTML = '<i class="fas fa-pause"></i> <span>Playing</span>';
}
}
// Record play analytics
recordTrackPlay(trackId);
// Show notification
showNotification('🎵 Now playing: ' + title, 'success');
console.log('✅ Track playing successfully');
} else {
console.warn('❌ Global player returned false');
showNotification('Failed to start playback. Please try again.', 'error');
}
} catch (error) {
console.warn('❌ Error during playback:', error);
showNotification('Playback error: ' + error.message, 'error');
}
}
function playTrackFromWaveform(trackId, audioUrl, title, artist) {
playTrack(trackId, audioUrl, title, artist);
}
function addToCart(trackId, title, price, artistPlan = 'free') {
if (!<?= $user_id ? 'true' : 'false' ?>) {
showNotification('Please log in to add tracks to cart', 'warning');
return;
}
console.log('🛒 Adding to cart:', { trackId, title, price, artistPlan });
// Add loading state to button
const button = event.target.closest('.btn-cart');
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Adding...';
button.disabled = true;
// Send to cart.php via POST with artist plan info
const formData = new FormData();
formData.append('track_id', trackId);
formData.append('action', 'add');
formData.append('artist_plan', artistPlan);
fetch('cart.php', {
method: 'POST',
body: formData
})
.then(response => response.text())
.then(responseText => {
console.log('🛒 Raw cart response:', responseText);
let data;
try {
data = JSON.parse(responseText);
console.log('🛒 Parsed cart response:', data);
} catch (e) {
console.warn('🛒 Failed to parse JSON response:', e);
console.warn('🛒 Raw response was:', responseText);
throw new Error('Invalid JSON response from cart');
}
if (!data.success) {
throw new Error(data.message || 'Failed to add to cart');
}
if (price == 0) {
// Free track - make it feel like a premium purchase experience!
showNotification(`🎵 "${title}" added to cart for FREE! 🛒 Ready to purchase and own!`, 'success');
} else {
// Paid track added to cart
const revenueInfo = (artistPlan === 'free') ? ' (Platform Revenue)' : '';
showNotification(`"${title}" added to cart! ($${price})${revenueInfo}`, 'success');
}
// Update cart UI if there's a cart counter
const cartCounter = document.querySelector('.cart-count, .cart-counter');
if (cartCounter && data.cart_count) {
cartCounter.textContent = data.cart_count;
}
// Log debug info if available
if (data.debug) {
if (defined('DEVELOPMENT_MODE') && DEVELOPMENT_MODE) {
console.log('🛒 Debug info:', data.debug);
}
}
// Change button to "Added" state temporarily
button.innerHTML = '<i class="fas fa-check"></i> Added!';
button.classList.add('added');
setTimeout(() => {
button.innerHTML = originalText;
button.classList.remove('added');
button.disabled = false;
}, 2000);
})
.catch(error => {
console.warn('🛒 Cart error:', error);
showNotification('Failed to add to cart: ' + error.message, 'error');
// Restore button
button.innerHTML = originalText;
button.disabled = false;
});
}
function toggleFollow(userId, button) {
if (!<?= $user_id ? 'true' : 'false' ?>) {
showNotification('Please log in to follow artists', 'warning');
return;
}
console.log('👤 Toggling follow for user:', userId);
// Add loading state
button.style.pointerEvents = 'none';
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'follow', user_id: userId })
})
.then(response => {
console.log('👤 Follow API response status:', response.status);
return response.json();
})
.then(data => {
console.log('👤 Follow API response data:', data);
if (data.success) {
button.classList.toggle('following');
const isFollowing = button.classList.contains('following');
button.innerHTML = `<i class="fas fa-user-${isFollowing ? 'check' : 'plus'}"></i> ${isFollowing ? 'Following' : 'Follow'}`;
// Show success notification
const action = isFollowing ? 'followed' : 'unfollowed';
showNotification(`Artist ${action}!`, 'success');
} else {
showNotification(data.message || 'Failed to follow artist', 'error');
}
})
.catch(error => {
console.warn('👤 Follow error:', error);
showNotification('Failed to follow artist. Please try again.', 'error');
})
.finally(() => {
// Restore button
button.style.pointerEvents = 'auto';
if (!button.innerHTML.includes('Following') && !button.innerHTML.includes('Follow')) {
button.innerHTML = originalText;
}
});
}
// Track play count functionality
function recordTrackPlay(trackId) {
// Only record if not already recorded recently
const lastPlayed = sessionStorage.getItem(`played_${trackId}`);
const now = Date.now();
if (!lastPlayed || (now - parseInt(lastPlayed)) > 30000) { // 30 seconds minimum between plays
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'play', track_id: trackId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
sessionStorage.setItem(`played_${trackId}`, now.toString());
console.log('🎵 Play count recorded for track:', trackId);
}
})
.catch(error => {
console.warn('🎵 Play count error:', error);
});
}
}
// Play button functionality is now handled by inline onclick="togglePlayPause()" calls
// This prevents conflicts and ensures proper parameter passing
document.addEventListener('DOMContentLoaded', function() {
console.log('🎵 Community Fixed - Initialized (play buttons use inline handlers)');
// Initialize like button states from server data
// The buttons are already set with the 'liked' class from PHP, but we verify here
document.querySelectorAll('.action-icon-btn[onclick*="toggleLike"]').forEach(button => {
const onclickAttr = button.getAttribute('onclick');
const trackIdMatch = onclickAttr.match(/toggleLike\((\d+)/);
if (trackIdMatch) {
const trackId = parseInt(trackIdMatch[1]);
// Verify the like state matches the server state
// The button should already have the 'liked' class if user_liked > 0
if (button.classList.contains('liked')) {
console.log('❤️ Like button initialized as liked for track:', trackId);
}
}
});
});
// 🚀 VIRAL TRACK SHARING SYSTEM
function showShareModal(trackId, title, artist) {
const trackUrl = `https://soundstudiopro.com/track/${trackId}`;
const shareText = `🎵 Check out "${title}" by ${artist}`;
// Update share URL input
document.getElementById('shareUrl').value = trackUrl;
// Update social share links
const platforms = {
twitter: `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(trackUrl)}`,
facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(trackUrl)}`,
whatsapp: `https://wa.me/?text=${encodeURIComponent(shareText + ' ' + trackUrl)}`,
linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(trackUrl)}`,
discord: `https://discord.com/api/oauth2/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=${encodeURIComponent(trackUrl)}&response_type=code&scope=webhook.incoming`
};
// Update platform buttons
document.querySelector('[data-platform="twitter"]').onclick = () => openShare(platforms.twitter, 'twitter', trackId);
document.querySelector('[data-platform="facebook"]').onclick = () => openShare(platforms.facebook, 'facebook', trackId);
document.querySelector('[data-platform="whatsapp"]').onclick = () => openShare(platforms.whatsapp, 'whatsapp', trackId);
document.querySelector('[data-platform="linkedin"]').onclick = () => openShare(platforms.linkedin, 'linkedin', trackId);
document.querySelector('[data-platform="discord"]').onclick = () => copyDiscordLink(trackUrl, trackId);
document.getElementById('shareModal').style.display = 'block';
document.body.style.overflow = 'hidden';
}
function openShare(url, platform, trackId) {
window.open(url, '_blank', 'width=600,height=400');
recordShare(trackId, platform);
closeShareModal();
}
function copyShareUrl() {
const shareUrl = document.getElementById('shareUrl');
shareUrl.select();
shareUrl.setSelectionRange(0, 99999);
navigator.clipboard.writeText(shareUrl.value);
const copyBtn = document.querySelector('.copy-btn');
const originalText = copyBtn.textContent;
copyBtn.textContent = 'Copied!';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = originalText;
copyBtn.classList.remove('copied');
}, 2000);
// Record share
const trackId = shareUrl.value.split('/track/')[1];
recordShare(trackId, 'copy-link');
}
function copyDiscordLink(url, text, trackId) {
const discordText = `🎵 ${text}\\n${url}`;
navigator.clipboard.writeText(discordText).then(() => {
recordShare(trackId, 'discord');
showNotification('Discord message copied! Paste it in your server 🚀', 'success');
});
}
function copyInstagramLink(url, text, trackId) {
navigator.clipboard.writeText(url).then(() => {
recordShare(trackId, 'instagram');
showNotification('Link copied! Add it to your Instagram story 📸', 'success');
});
}
function recordShare(trackId, platform) {
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'share',
track_id: trackId,
platform: platform
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log(`🚀 Share recorded: ${platform} for track ${trackId}`);
// Update share count in UI
const shareButton = document.querySelector(`[onclick*="${trackId}"] .social-count`);
if (shareButton) {
const currentCount = parseInt(shareButton.textContent);
shareButton.textContent = currentCount + 1;
}
}
})
.catch(error => {
console.warn('🚀 Share recording error:', error);
});
}
// Handle shared track highlighting from URL
function handleSharedTrack() {
const urlParams = new URLSearchParams(window.location.search);
const trackId = urlParams.get('track');
if (trackId) {
// Find the track card
const trackCard = document.querySelector(`[data-track-id="${trackId}"]`)?.closest('.track-card');
if (trackCard) {
// Scroll to the track
setTimeout(() => {
trackCard.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
// Add highlight effect
trackCard.classList.add('highlighted');
// Optional: Auto-play the track
const playButton = trackCard.querySelector('.play-track-btn');
if (playButton) {
setTimeout(() => {
playButton.click();
}, 1000);
}
// Remove highlight after animation
setTimeout(() => {
trackCard.classList.remove('highlighted');
}, 3000);
}, 500);
showNotification('🎵 Shared track found! Playing now...', 'success');
} else {
showNotification('Track not found on this page', 'warning');
}
}
}
// Initialize shared track handling
document.addEventListener('DOMContentLoaded', function() {
handleSharedTrack();
});
// Close modal when clicking outside
document.addEventListener('click', function(e) {
if (e.target.classList.contains('share-modal')) {
closeShareModal();
}
});
// DJ Mixing Features
function highlightCompatibleTracks(currentBpm, currentKey) {
const tracks = document.querySelectorAll('.track-card');
tracks.forEach(track => {
const bpmElement = track.querySelector('.bpm-value');
const keyElement = track.querySelector('.key-value');
if (bpmElement && keyElement) {
const trackBpm = parseInt(bpmElement.textContent);
const trackKey = keyElement.textContent;
// BPM compatibility (within 6% for mixing)
const bpmDiff = Math.abs(trackBpm - currentBpm);
const bpmCompatible = bpmDiff <= (currentBpm * 0.06);
// Key compatibility (same key, relative major/minor, perfect 5th)
const keyCompatible = isKeyCompatible(currentKey, trackKey);
// Add visual indicators
if (bpmCompatible) {
bpmElement.parentElement.classList.add('bpm-compatible');
}
if (keyCompatible) {
keyElement.parentElement.classList.add('key-compatible');
}
if (bpmCompatible && keyCompatible) {
track.classList.add('perfect-mix-match');
}
}
});
}
function isKeyCompatible(key1, key2) {
// Simplified key compatibility - same key, relative major/minor
if (key1 === key2) return true;
// Basic major/minor relative matching
const keyMap = {
'C major': 'A minor',
'A minor': 'C major',
'G major': 'E minor',
'E minor': 'G major',
'D major': 'B minor',
'B minor': 'D major',
'A major': 'F# minor',
'F# minor': 'A major',
'E major': 'C# minor',
'C# minor': 'E major',
'F major': 'D minor',
'D minor': 'F major',
'Bb major': 'G minor',
'G minor': 'Bb major'
};
return keyMap[key1] === key2;
}
function calculateBpmRange(bpm) {
const range = Math.round(bpm * 0.06);
return {
min: bpm - range,
max: bpm + range,
half: Math.round(bpm / 2),
double: bpm * 2
};
}
// Enhanced track clicking for DJ mode
function selectTrackForDjMode(trackElement) {
// Clear previous selections
document.querySelectorAll('.track-card.dj-selected').forEach(card => {
card.classList.remove('dj-selected');
});
// Mark as selected
trackElement.classList.add('dj-selected');
// Get technical info
const bpmElement = trackElement.querySelector('.bpm-value');
const keyElement = trackElement.querySelector('.key-value');
if (bpmElement && keyElement) {
const bpm = parseInt(bpmElement.textContent);
const key = keyElement.textContent;
// Highlight compatible tracks
highlightCompatibleTracks(bpm, key);
// Show DJ info panel
showDjMixingPanel(bpm, key);
}
}
function showDjMixingPanel(bpm, key) {
const bpmRange = calculateBpmRange(bpm);
// Create or update DJ panel
let djPanel = document.getElementById('dj-mixing-panel');
if (!djPanel) {
djPanel = document.createElement('div');
djPanel.id = 'dj-mixing-panel';
djPanel.className = 'dj-mixing-panel';
document.body.appendChild(djPanel);
}
djPanel.innerHTML = `
<div class="dj-panel-header">
<h3>🎧 DJ Mixing Info</h3>
<button onclick="closeDjPanel()" class="close-dj-panel">×</button>
</div>
<div class="dj-panel-content">
<div class="current-track-info">
<div class="dj-info-item">
<label>Current BPM:</label>
<span class="dj-bpm">${bpm}</span>
</div>
<div class="dj-info-item">
<label>Current Key:</label>
<span class="dj-key">${key}</span>
</div>
</div>
<div class="mixing-suggestions">
<h4>🎯 Mixing Compatibility</h4>
<div class="bpm-suggestions">
<p><strong>BPM Range:</strong> ${bpmRange.min} - ${bpmRange.max}</p>
<p><strong>Half Time:</strong> ${bpmRange.half} BPM</p>
<p><strong>Double Time:</strong> ${bpmRange.double} BPM</p>
</div>
<div class="key-suggestions">
<p><strong>Compatible Keys:</strong> Same key, relative major/minor</p>
</div>
</div>
</div>
`;
djPanel.style.display = 'block';
}
function closeDjPanel() {
const djPanel = document.getElementById('dj-mixing-panel');
if (djPanel) {
djPanel.style.display = 'none';
}
// Clear all highlighting
document.querySelectorAll('.track-card').forEach(card => {
card.classList.remove('dj-selected', 'perfect-mix-match');
});
document.querySelectorAll('.bpm-compatible').forEach(el => {
el.classList.remove('bpm-compatible');
});
document.querySelectorAll('.key-compatible').forEach(el => {
el.classList.remove('key-compatible');
});
}
// Add click handlers for DJ mode
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.dj-tech-grid').forEach(grid => {
grid.addEventListener('click', function(e) {
e.preventDefault();
const trackCard = this.closest('.track-card');
selectTrackForDjMode(trackCard);
});
});
});
// Premium Rating System
function rateTrack(trackId, rating, starElement) {
if (!<?= $user_id ? 'true' : 'false' ?>) {
showNotification('Please log in to rate tracks', 'warning');
return;
}
console.log('⭐ Rating track:', trackId, 'with', rating, 'stars');
// Add loading state
const ratingContainer = starElement.closest('.star-rating');
ratingContainer.style.pointerEvents = 'none';
fetch('/api_social.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'rate',
track_id: trackId,
rating: rating
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update visual rating
const stars = ratingContainer.querySelectorAll('.star');
stars.forEach((star, index) => {
star.classList.remove('filled', 'user-rated');
if (index < data.average_rating) {
star.classList.add('filled');
}
if (index < rating) {
star.classList.add('user-rated');
}
});
// Update stats
const avgElement = ratingContainer.closest('.track-rating-section').querySelector('.avg-rating');
const countElement = ratingContainer.closest('.track-rating-section').querySelector('.rating-count');
if (avgElement) avgElement.textContent = data.average_rating.toFixed(1) + '/10';
if (countElement) countElement.textContent = `(${data.rating_count} ratings)`;
showNotification(`⭐ Rated ${rating}/10 stars!`, 'success');
} else {
showNotification(data.message || 'Failed to rate track', 'error');
}
})
.catch(error => {
console.warn('⭐ Rating error:', error);
showNotification('Failed to rate track. Please try again.', 'error');
})
.finally(() => {
ratingContainer.style.pointerEvents = 'auto';
});
}
// Enhanced Play/Pause Logic
// Track currently playing track ID for state management
let currentlyPlayingTrackId = null;
function togglePlayPause(button, trackId, audioUrl, title, artist) {
const isCurrentlyPlaying = currentlyPlayingTrackId === trackId;
const globalPlayer = window.enhancedGlobalPlayer;
if (isCurrentlyPlaying && globalPlayer && globalPlayer.isPlaying) {
// Pause current track
globalPlayer.pause();
button.innerHTML = '<i class="fas fa-play"></i><span>Play</span>';
button.classList.remove('playing');
// Remove playing states
document.querySelectorAll('.track-card').forEach(card => {
card.classList.remove('currently-playing');
});
currentlyPlayingTrackId = null;
showNotification('⏸️ Paused', 'success');
} else {
// Play new track or resume
if (globalPlayer && typeof globalPlayer.playTrack === 'function') {
globalPlayer.playTrack(audioUrl, title, artist);
// Update all play buttons
document.querySelectorAll('.play-track-btn').forEach(btn => {
btn.classList.remove('playing');
btn.innerHTML = '<i class="fas fa-play"></i><span>Play</span>';
});
// Update current button
button.classList.add('playing');
button.innerHTML = '<i class="fas fa-pause"></i><span>Playing</span>';
// Update track cards
document.querySelectorAll('.track-card').forEach(card => {
card.classList.remove('currently-playing');
});
const currentCard = document.querySelector(`[data-track-id="${trackId}"]`);
if (currentCard) {
currentCard.classList.add('currently-playing');
}
currentlyPlayingTrackId = trackId;
recordTrackPlay(trackId);
showNotification('🎵 Now playing: ' + title, 'success');
} else {
showNotification('Player not ready, please try again', 'error');
}
}
}
// View Track Charts Function
function viewTrackCharts(trackId, genre) {
// Build chart URL with track context
const chartUrl = `/charts.php?track=${trackId}&genre=${encodeURIComponent(genre)}&highlight=true`;
// Open charts in new tab or redirect
if (confirm('View this track in the global charts?')) {
window.open(chartUrl, '_blank');
}
}
// Charts Modal Functions
function showChartsModal(trackId, position, title, genre) {
document.getElementById('currentPosition').textContent = '#' + position;
document.getElementById('trackTitle').textContent = title;
document.getElementById('genreRanking').textContent = '#' + Math.floor(Math.random() * 20 + 1) + ' in ' + genre;
document.getElementById('weeklyPlays').textContent = (Math.floor(Math.random() * 1000) + 100).toLocaleString();
document.getElementById('totalRating').textContent = (Math.random() * 3 + 7).toFixed(1) + '/10';
// Random position change
const change = Math.floor(Math.random() * 20) - 10;
const changeElement = document.getElementById('positionChange');
if (change > 0) {
changeElement.textContent = '↗ +' + change + ' this week';
changeElement.className = 'position-change';
} else if (change < 0) {
changeElement.textContent = '↘ ' + change + ' this week';
changeElement.className = 'position-change down';
} else {
changeElement.textContent = '→ No change';
changeElement.className = 'position-change';
}
document.getElementById('chartsModal').style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closeChartsModal() {
document.getElementById('chartsModal').style.display = 'none';
document.body.style.overflow = 'auto';
}
function openFullCharts() {
closeChartsModal();
window.open('/charts.php', '_blank');
}
// Premium Modal Functions
function showPremiumModal() {
document.getElementById('premiumModal').style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closePremiumModal() {
document.getElementById('premiumModal').style.display = 'none';
document.body.style.overflow = 'auto';
}
function upgradeToPremium() {
closePremiumModal();
window.location.href = '/upgrade.php';
}
// Close modals when clicking outside
window.onclick = function(event) {
const chartsModal = document.getElementById('chartsModal');
const premiumModal = document.getElementById('premiumModal');
if (event.target === chartsModal) {
closeChartsModal();
}
if (event.target === premiumModal) {
closePremiumModal();
}
}
// Toggle Lyrics Function
function toggleLyrics(trackId) {
const lyricsContent = document.getElementById('lyrics-' + trackId);
const toggleButton = lyricsContent.previousElementSibling;
const toggleSpan = toggleButton.querySelector('span');
const toggleIcon = toggleButton.querySelector('.toggle-icon');
if (lyricsContent.style.display === 'none') {
lyricsContent.style.display = 'block';
lyricsContent.classList.add('expanded');
toggleButton.classList.add('active');
toggleSpan.textContent = 'Hide Lyrics';
} else {
lyricsContent.classList.remove('expanded');
toggleButton.classList.remove('active');
toggleSpan.textContent = 'Show Lyrics';
setTimeout(() => {
lyricsContent.style.display = 'none';
}, 400);
}
}
// Add to Playlist Function
function addToPlaylist(trackId) {
// TODO: Implement playlist functionality
showNotification('🎵 Playlist feature coming soon!', 'info');
}
// Add to Cart Function
function addToCart(trackId, title, price, button) {
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
fetch('/cart.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `action=add&track_id=${trackId}&title=${encodeURIComponent(title)}&price=${price}`
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification(`🛒 "${title}" added to cart!`, 'success');
// Update cart counter if it exists - use the actual count from the API response
const cartCounts = document.querySelectorAll('.cart-count, .cart-counter');
if (cartCounts.length > 0 && data.cart_count !== undefined) {
cartCounts.forEach(count => {
count.textContent = data.cart_count;
// Show the badge if count > 0
if (data.cart_count > 0) {
count.style.display = 'flex';
if (count.classList.contains('cart-counter')) {
count.style.display = 'block';
}
} else {
count.style.display = 'none';
}
});
} else if (cartCounts.length > 0) {
// Fallback: manually increment if API didn't return count
cartCounts.forEach(count => {
const currentCount = parseInt(count.textContent) || 0;
count.textContent = currentCount + 1;
count.style.display = 'flex';
if (count.classList.contains('cart-counter')) {
count.style.display = 'block';
}
});
}
// 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 {
showNotification(data.message || 'Failed to add to cart', 'error');
}
})
.catch(error => {
console.warn('Cart error:', error);
showNotification('Network error', 'error');
})
.finally(() => {
button.disabled = false;
button.innerHTML = '<i class="fas fa-shopping-cart"></i>';
});
}
// Record Track Play
function recordTrackPlay(trackId) {
fetch('/api_social.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `action=play&track_id=${trackId}`
})
.catch(error => console.warn('Play tracking error:', error));
}
// Toggle Follow Function
function toggleFollow(userId, button) {
const isFollowing = button.classList.contains('following');
const action = isFollowing ? 'unfollow' : 'follow';
fetch('/api_social.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `action=${action}&user_id=${userId}`
})
.then(response => response.json())
.then(data => {
if (data.success) {
button.classList.toggle('following');
const icon = button.querySelector('i');
const text = button.querySelector('span') || button.childNodes[1];
if (isFollowing) {
icon.className = 'fas fa-user-plus';
if (text) text.textContent = ' Follow';
showNotification('Unfollowed artist', 'info');
} else {
icon.className = 'fas fa-user-check';
if (text) text.textContent = ' Following';
showNotification('Following artist!', 'success');
}
} else {
showNotification('Failed to update follow status', 'error');
}
})
.catch(error => {
console.warn('Follow error:', error);
showNotification('Network error', 'error');
});
}
</script>
<script>
// Initialize enhanced global player integration
document.addEventListener('DOMContentLoaded', function() {
console.log('🎵 Community page loaded, waiting for enhanced global player...');
// Wait for enhanced global player to be ready, then enable play buttons
waitForEnhancedPlayerCallback(() => {
enablePlayButtons();
});
});
</script>
<!-- Include the existing modals and JavaScript functions -->
<!-- This ensures all functionality is preserved -->
<!-- Stems Modal -->
<div id="stemsModal" class="variations-modal">
<div class="variations-content">
<div class="variations-header">
<h2 class="variations-title">🎵 Track Stems</h2>
<button class="close-variations" onclick="closeStemsModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div id="stemsGrid" class="variations-grid">
<!-- Stems will be loaded here -->
</div>
</div>
</div>
<!-- Variations Modal -->
<div id="variationsModal" class="variations-modal">
<div class="variations-content">
<div class="variations-header">
<h2 class="variations-title"><?= t('library.variations.title') ?></h2>
<button class="close-variations" onclick="closeVariations()">
<i class="fas fa-times"></i>
</button>
</div>
<div id="variationsGrid" class="variations-grid">
<!-- Variations will be loaded here -->
</div>
<div class="variations-footer">
<div class="variations-info">
<i class="fas fa-info-circle"></i>
<?= t('library.variations.info') ?>
</div>
<div class="variations-actions">
<button class="variations-btn cancel" onclick="closeVariations()">
<?= t('library.variations.cancel') ?>
</button>
<button id="saveVariationBtn" class="variations-btn save" onclick="saveVariationSelection()" disabled>
<?= t('library.variations.save_selection') ?>
</button>
</div>
</div>
</div>
</div>
<!-- Lyrics Modal -->
<div id="lyricsModal" class="lyrics-modal">
<div class="lyrics-content">
<div class="lyrics-header">
<h2 class="lyrics-title">Track Lyrics</h2>
<button class="close-lyrics" onclick="closeLyrics()">
<i class="fas fa-times"></i>
</button>
</div>
<div id="lyricsContent" class="lyrics-body">
<!-- Lyrics will be loaded here -->
</div>
<div class="lyrics-footer">
<div class="lyrics-actions">
<button class="lyrics-btn cancel" onclick="closeLyrics()">
Close
</button>
<button class="lyrics-btn copy" onclick="copyLyrics()">
<i class="fas fa-copy"></i> Copy Lyrics
</button>
</div>
</div>
</div>
</div>
<!-- Download Modal -->
<div id="downloadModal" class="download-modal">
<div class="download-content">
<div class="download-header">
<h2 class="download-title">Download Track</h2>
<button class="close-download" onclick="closeDownload()">
<i class="fas fa-times"></i>
</button>
</div>
<div id="downloadContent" class="download-body">
<!-- Download options will be loaded here -->
</div>
<div class="download-footer">
<div class="download-actions">
<button class="download-btn cancel" onclick="closeDownload()">
Cancel
</button>
</div>
</div>
</div>
</div>
<script>
// Translation strings for JavaScript
const libraryTranslations = {
play: '<?= addslashes(t('library.variations.play')) ?>',
download: '<?= addslashes(t('library.variations.download')) ?>',
select: '<?= addslashes(t('library.variations.select')) ?>',
selected: '<?= addslashes(t('library.variations.selected')) ?>',
variation: '<?= addslashes(t('library.variations.variation')) ?>',
no_variations: '<?= addslashes(t('library.variations.no_variations')) ?>',
audio_not_available: '<?= addslashes(t('library.variations.audio_not_available')) ?>',
untitled_track: '<?= addslashes(t('library.variations.untitled_track')) ?>',
// Crates translations
crates_empty_title: '<?= addslashes(t('library.crates.empty_title')) ?>',
crates_empty_desc: '<?= addslashes(t('library.crates.empty_desc')) ?>',
crates_create_first: '<?= addslashes(t('library.crates.create_first')) ?>',
crates_set_duration: '<?= addslashes(t('library.crates.set_duration')) ?>',
crates_tracks_needed: '<?= addslashes(t('library.crates.tracks_needed')) ?>',
crates_ready: '<?= addslashes(t('library.crates.ready')) ?>',
crates_ready_short: '<?= addslashes(t('library.crates.ready_short')) ?>',
crates_tracks: '<?= addslashes(t('library.crates.tracks')) ?>',
crates_min_set: '<?= addslashes(t('library.crates.min_set')) ?>',
crates_no_tracks: '<?= addslashes(t('library.crates.no_tracks')) ?>',
crates_add_tracks_desc: '<?= addslashes(t('library.crates.add_tracks_desc')) ?>',
crates_download: '<?= addslashes(t('library.crates.download')) ?>',
crates_download_all: '<?= addslashes(t('library.crates.download_all')) ?>',
crates_add_to_cart: '<?= addslashes(t('common.add_to_cart')) ?>',
free: '<?= addslashes(t('common.free')) ?>',
added_to_cart: '<?= addslashes(t('notification.added_to_cart')) ?>',
added_to_cart_free: '<?= addslashes(t('notification.added_to_cart_free')) ?>',
crates_remove: '<?= addslashes(t('library.crates.remove')) ?>',
crates_drag_reorder: '<?= addslashes(t('library.crates.drag_reorder')) ?>',
crates_order_updated: '<?= addslashes(t('library.crates.order_updated')) ?>',
crates_updating_order: '<?= addslashes(t('library.crates.updating_order')) ?>',
crates_error_loading: '<?= addslashes(t('library.crates.error_loading')) ?>',
crates_error_loading_crate: '<?= addslashes(t('library.crates.error_loading_crate')) ?>',
crates_error_loading_desc: '<?= addslashes(t('library.crates.error_loading_desc')) ?>',
crates_track_added: '<?= addslashes(t('library.crates.track_added')) ?>',
crates_track_removed: '<?= addslashes(t('library.crates.track_removed')) ?>',
crates_crate_deleted: '<?= addslashes(t('library.crates.crate_deleted')) ?>',
crates_crate_created: '<?= addslashes(t('library.crates.crate_created')) ?>',
crates_already_added: '<?= addslashes(t('library.crates.already_added')) ?>',
crates_loading_tracks: '<?= addslashes(t('library.crates.loading_tracks')) ?>',
crates_delete_confirm: '<?= addslashes(t('library.crates.delete_confirm')) ?>',
crates_remove_confirm: '<?= addslashes(t('library.crates.remove_confirm')) ?>',
crates_loading: '<?= addslashes(t('library.crates.loading')) ?>',
crates_details: '<?= addslashes(t('library.crates.details')) ?>',
crates_name_required: '<?= addslashes(t('library.crates.name_required')) ?>',
crates_visibility_public: '<?= addslashes(t('library.crates.visibility_public')) ?>',
crates_visibility_private: '<?= addslashes(t('library.crates.visibility_private')) ?>',
crates_share: '<?= addslashes(t('library.crates.share')) ?>',
crates_link_copied: '<?= addslashes(t('library.crates.link_copied')) ?>',
crates_now_public: '<?= addslashes(t('library.crates.now_public')) ?>',
crates_now_private: '<?= addslashes(t('library.crates.now_private')) ?>',
crates_track_public: '<?= addslashes(t('library.crates.track_public')) ?>',
crates_track_private: '<?= addslashes(t('library.crates.track_private')) ?>',
crates_desc_public: '<?= addslashes(t('library.crates.desc_public')) ?>',
crates_desc_private: '<?= addslashes(t('library.crates.desc_private')) ?>',
crates_desc_saved: '<?= addslashes(t('library.crates.desc_saved')) ?>',
crates_add_description: '<?= addslashes(t('library.crates.add_description')) ?>',
crates_save_description: '<?= addslashes(t('library.crates.save_description')) ?>',
crates_renamed: '<?= addslashes(t('library.crates.crate_renamed')) ?>',
crates_rename: '<?= addslashes(t('library.crates.rename')) ?>',
crates_rename_modal_title: '<?= addslashes(t('library.crates.rename_modal_title')) ?>',
crates_new_name: '<?= addslashes(t('library.crates.new_name')) ?>',
// Menu translations
view_track_page: '<?= addslashes(t('library.menu.view_track_page')) ?>',
delete_track: '<?= addslashes(t('library.menu.delete_track')) ?>',
delete_confirm: '<?= addslashes(t('library.menu.delete_confirm')) ?>',
// Image upload/download translations
image_download: '<?= addslashes(t('library.image.download')) ?>',
image_upload_success: '<?= addslashes(t('library.image.upload_success')) ?>',
image_upload_failed: '<?= addslashes(t('library.image.upload_failed')) ?>',
image_upload_failed_unknown: '<?= addslashes(t('library.image.upload_failed_unknown')) ?>',
image_upload_failed_retry: '<?= addslashes(t('library.image.upload_failed_retry')) ?>',
image_invalid_file_type: '<?= addslashes(t('library.image.invalid_file_type')) ?>',
image_file_too_large: '<?= addslashes(t('library.image.file_too_large')) ?>',
// Share modal translations
share_title: '<?= addslashes(t('library.share.title')) ?>',
share_important_notice: '<?= addslashes(t('library.share.important_notice')) ?>',
share_private_warning: '<?= addslashes(t('library.share.private_warning')) ?>',
share_expiration_label: '<?= addslashes(t('library.share.expiration_label')) ?>',
share_expiration_1hour: '<?= addslashes(t('library.share.expiration_1hour')) ?>',
share_expiration_6hours: '<?= addslashes(t('library.share.expiration_6hours')) ?>',
share_expiration_24hours: '<?= addslashes(t('library.share.expiration_24hours')) ?>',
share_expiration_7days: '<?= addslashes(t('library.share.expiration_7days')) ?>',
share_expiration_30days: '<?= addslashes(t('library.share.expiration_30days')) ?>',
share_share_with_link: '<?= addslashes(t('library.share.share_with_link')) ?>',
share_accessible_via_link: '<?= addslashes(t('library.share.accessible_via_link')) ?>',
share_make_public_and_share: '<?= addslashes(t('library.share.make_public_and_share')) ?>',
share_cancel: '<?= addslashes(t('library.share.cancel')) ?>',
share_success: '<?= addslashes(t('library.share.success')) ?>',
share_failed: '<?= addslashes(t('library.share.failed')) ?>',
share_make_public_success: '<?= addslashes(t('library.share.make_public_success')) ?>',
share_make_public_failed: '<?= addslashes(t('library.share.make_public_failed')) ?>'
};
// Global variables for variations functionality
window.trackVariations = [];
window.currentTrackId = null;
window.selectedVariationIndex = null;
// Show variations modal
function showVariations(trackId) {
console.log('🎵 Showing variations for track:', trackId);
// Get track data from PHP
const trackCard = document.querySelector(`[data-track-id="${trackId}"]`);
if (!trackCard) {
console.warn('🎵 Track card not found for track ID:', trackId);
alert('Track not found. Please refresh the page and try again.');
return;
}
console.log('🎵 Track card found:', trackCard);
// Get variations data from PHP
const variationsData = trackCard.getAttribute('data-variations');
console.log('🎵 Raw variations data:', variationsData);
// Debug: Check if variations data exists
if (!variationsData) {
console.warn('🎵 No variations data attribute found');
alert('No variations data found for this track.');
return;
}
if (variationsData === '[]' || variationsData === 'null') {
console.warn('🎵 Variations data is empty');
alert('No variations available for this track.');
return;
}
try {
if (typeof window.trackVariations === 'undefined') {
window.trackVariations = [];
}
window.trackVariations = JSON.parse(variationsData);
console.log('🎵 Parsed variations:', window.trackVariations);
if (!Array.isArray(window.trackVariations) || window.trackVariations.length === 0) {
console.warn('🎵 No variations in array');
alert(libraryTranslations.no_variations);
return;
}
window.currentTrackId = trackId;
// Get main track title from the track card
const trackTitleElement = trackCard.querySelector('.track-name');
window.mainTrackTitle = trackTitleElement ? trackTitleElement.textContent.trim() : libraryTranslations.untitled_track;
// Get current selection
const currentSelection = trackCard.getAttribute('data-selected-variation') || '0';
window.selectedVariationIndex = parseInt(currentSelection);
console.log('🎵 About to populate variations grid');
// Populate variations grid
populateVariationsGrid();
console.log('🎵 About to show modal');
// Show modal
const modal = document.getElementById('variationsModal');
if (!modal) {
console.warn('🎵 Variations modal not found');
alert('Modal not found. Please refresh the page and try again.');
return;
}
modal.classList.add('active');
console.log('🎵 Modal should now be visible');
// Force modal to be visible and on top
modal.style.zIndex = '999999';
modal.style.position = 'fixed';
modal.style.top = '0';
modal.style.left = '0';
modal.style.width = '100%';
modal.style.height = '100%';
modal.style.display = 'flex';
modal.style.alignItems = 'center';
modal.style.justifyContent = 'center';
console.log('🎵 Modal styles applied:', {
zIndex: modal.style.zIndex,
position: modal.style.position,
display: modal.style.display
});
// Close on outside click (only if clicking the backdrop, not the content)
const existingHandler = modal._closeHandler;
if (existingHandler) {
modal.removeEventListener('click', existingHandler);
}
modal._closeHandler = function(e) {
// Only close if clicking the backdrop (the modal itself), not the content
if (e.target === modal) {
closeVariations();
}
};
modal.addEventListener('click', modal._closeHandler);
// Prevent content clicks from closing
const content = modal.querySelector('.variations-content');
if (content) {
content.addEventListener('click', function(e) {
e.stopPropagation();
});
}
} catch (error) {
console.warn('🎵 Error parsing variations data:', error);
alert('Error loading variations. Please refresh the page and try again.');
}
}
// Populate variations grid
function populateVariationsGrid() {
const grid = document.getElementById('variationsGrid');
grid.innerHTML = '';
window.trackVariations.forEach((variation, index) => {
const card = document.createElement('div');
card.className = `variation-card ${index === window.selectedVariationIndex ? 'selected' : ''}`;
card.onclick = () => selectVariation(index);
// Handle duration - check if it exists and is valid
let duration = '0:00';
if (variation.duration && variation.duration > 0) {
const minutes = Math.floor(variation.duration / 60);
const seconds = Math.floor(variation.duration % 60);
duration = minutes + 'm ' + seconds + 's';
} else if (variation.metadata) {
// Try to get duration from metadata
try {
const meta = typeof variation.metadata === 'string' ? JSON.parse(variation.metadata) : variation.metadata;
if (meta && meta.duration && meta.duration > 0) {
const minutes = Math.floor(meta.duration / 60);
const seconds = Math.floor(meta.duration % 60);
duration = minutes + 'm ' + seconds + 's';
}
} catch (e) {
console.warn('Error parsing variation metadata for duration:', e);
}
}
const tags = variation.tags ? variation.tags.split(',').slice(0, 3) : [];
card.innerHTML = `
<div class="variation-header">
<div class="variation-title">${window.mainTrackTitle || 'Variation ' + (index + 1)}</div>
<div class="variation-index">${index + 1}</div>
</div>
<div class="variation-duration">
<i class="fas fa-clock"></i> ${duration}
</div>
${tags.length > 0 ? `
<div class="variation-tags">
${tags.map(tag => `<span class="variation-tag">${tag.trim()}</span>`).join('')}
</div>
` : ''}
<div class="variation-actions">
<button class="variation-btn play" onclick="playVariation(${index})">
<i class="fas fa-play"></i> ${libraryTranslations.play}
</button>
<button class="variation-btn download" onclick="downloadVariation(${index})">
<i class="fas fa-download"></i> ${libraryTranslations.download}
</button>
<button class="variation-btn select ${index === window.selectedVariationIndex ? 'selected' : ''}" onclick="selectVariation(${index})">
<i class="fas fa-check"></i> ${index === window.selectedVariationIndex ? libraryTranslations.selected : libraryTranslations.select}
</button>
</div>
`;
grid.appendChild(card);
});
// Update save button state
updateSaveButton();
}
// Select variation
function selectVariation(index) {
window.selectedVariationIndex = index;
// Update visual selection
document.querySelectorAll('.variation-card').forEach((card, i) => {
card.classList.toggle('selected', i === index);
});
document.querySelectorAll('.variation-btn.select').forEach((btn, i) => {
btn.classList.toggle('selected', i === index);
btn.innerHTML = `<i class="fas fa-check"></i> ${i === index ? libraryTranslations.selected : libraryTranslations.select}`;
});
updateSaveButton();
}
// Play variation
async function playVariation(index) {
const variation = window.trackVariations[index];
if (!variation) return;
console.log('🎵 Playing variation:', variation);
// Wait for enhanced global player to be ready
await waitForEnhancedPlayer();
// Get signed URL from API to prevent URL sharing
const variationIndex = variation.variation_index !== undefined ? variation.variation_index : index;
try {
const response = await fetch('/api/get_audio_token.php?track_id=' + window.currentTrackId + '&variation=' + variationIndex);
const data = await response.json();
if (!data.success || !data.url) {
console.error('Failed to get signed audio URL:', data.error);
showNotification(libraryTranslations.audio_not_available, 'error');
return;
}
let finalAudioUrl = data.url;
console.log('🎵 Using signed proxy URL for variation:', finalAudioUrl);
// Use the enhanced global player (same as community.php)
if (typeof window.enhancedGlobalPlayer !== 'undefined' && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
console.log('🎵 Using enhanced global player for variation');
const trackTitle = window.mainTrackTitle || variation.title || libraryTranslations.variation + ' ' + (index + 1);
window.enhancedGlobalPlayer.playTrack(finalAudioUrl, trackTitle, '<?= htmlspecialchars($user_name) ?>');
} else {
console.warn('🎵 Enhanced global player not available for variation playback');
alert('Audio player not available. Please refresh the page and try again.');
}
} catch (err) {
console.error('Error getting audio token:', err);
showNotification(libraryTranslations.audio_not_available, 'error');
}
}
// Download variation
async function downloadVariation(index) {
const variation = window.trackVariations[index];
if (!variation) {
console.error('🎵 Variation not found at index:', index);
showNotification('Variation not found', 'error');
return;
}
console.log('🎵 Downloading variation:', variation);
// Get track information from modal or track card
let trackTitle = 'Untitled Track';
let artistName = '<?= htmlspecialchars($user_name) ?>';
let genre = '';
let bpm = '';
let key = '';
let numericalKey = '';
let mood = '';
// Try to get track info from the track card
if (window.currentTrackId) {
const trackCard = document.querySelector(`[data-track-id="${window.currentTrackId}"]`);
if (trackCard) {
const titleEl = trackCard.querySelector('.track-name');
if (titleEl) trackTitle = titleEl.textContent.trim();
const artistEl = trackCard.querySelector('.track-artist-name');
if (artistEl) artistName = artistEl.textContent.replace('By ', '').trim();
// Get metadata from track card data attribute if available
const trackPrompt = trackCard.getAttribute('data-prompt') || '';
// Try to extract numerical key from prompt (e.g., "Key: 6B – D Major")
if (trackPrompt) {
const keyMatch = trackPrompt.match(/key[:\s]+([0-9]+[A-G]?)\s*[–\-]?\s*/i);
if (keyMatch && keyMatch[1]) {
numericalKey = keyMatch[1].trim();
}
}
// Get metadata from displayed elements
const metadataRow = trackCard.querySelector('.track-metadata-row');
if (metadataRow) {
const metaItems = metadataRow.querySelectorAll('.meta-item');
metaItems.forEach(item => {
const text = item.textContent.trim();
if (text.includes('Genre:')) {
genre = text.replace('Genre:', '').trim();
} else if (text.includes('BPM:')) {
bpm = text.replace('BPM:', '').trim();
} else if (text.includes('Key:')) {
key = text.replace('Key:', '').trim();
} else if (text.includes('Mood:')) {
mood = text.replace('Mood:', '').trim();
}
});
}
}
}
// Sanitize filename
function sanitizeFilename(str) {
return str.replace(/[<>:"/\\|?*]/g, '').replace(/\s+/g, ' ').trim();
}
// Create comprehensive filename
const cleanTitle = sanitizeFilename(trackTitle);
const cleanArtist = sanitizeFilename(artistName);
const cleanGenre = sanitizeFilename(genre);
const cleanKey = sanitizeFilename(key);
const cleanMood = sanitizeFilename(mood);
const cleanNumericalKey = sanitizeFilename(numericalKey);
let filename = `${cleanArtist} - ${cleanTitle} (Variation ${index + 1})`;
// Add metadata to filename
const metadataParts = [];
if (cleanGenre) metadataParts.push(cleanGenre);
if (bpm) metadataParts.push(`${bpm} BPM`);
// Add numerical key first if available, then the musical key
if (cleanNumericalKey) metadataParts.push(cleanNumericalKey);
if (cleanKey) metadataParts.push(cleanKey);
if (cleanMood) metadataParts.push(cleanMood);
if (metadataParts.length > 0) {
filename += ` [${metadataParts.join(', ')}]`;
}
filename += '.mp3';
// Show loading message
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: rgba(102, 126, 234, 0.9);
color: white;
padding: 1rem 1.5rem;
border-radius: 8px;
font-weight: 600;
z-index: 10000;
backdrop-filter: blur(10px);
`;
toast.textContent = `⏳ Preparing download: ${filename}`;
document.body.appendChild(toast);
try {
// Get signed URL from API (same as play function)
const variationIndex = variation.variation_index !== undefined ? variation.variation_index : index;
const response = await fetch('/api/get_audio_token.php?track_id=' + window.currentTrackId + '&variation=' + variationIndex);
const data = await response.json();
if (!data.success || !data.url) {
console.error('Failed to get signed audio URL:', data.error);
toast.style.background = 'rgba(245, 101, 101, 0.9)';
toast.textContent = '❌ Failed to get download URL';
setTimeout(() => {
if (toast.parentNode) {
document.body.removeChild(toast);
}
}, 3000);
showNotification('Failed to prepare download. Please try again.', 'error');
return;
}
const downloadUrl = data.url;
console.log('🎵 Using signed proxy URL for download:', downloadUrl);
// Create a temporary link element to trigger download
const link = document.createElement('a');
link.href = downloadUrl;
link.download = filename;
link.target = '_blank';
// Add to DOM, click, and remove
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Update toast to success
toast.style.background = 'rgba(72, 187, 120, 0.9)';
toast.textContent = `✅ Downloading: ${filename}`;
setTimeout(() => {
if (toast.parentNode) {
document.body.removeChild(toast);
}
}, 3000);
} catch (error) {
console.error('🎵 Error downloading variation:', error);
toast.style.background = 'rgba(245, 101, 101, 0.9)';
toast.textContent = '❌ Download failed. Please try again.';
setTimeout(() => {
if (toast.parentNode) {
document.body.removeChild(toast);
}
}, 3000);
showNotification('Download failed. Please try again.', 'error');
}
}
// Update save button state
function updateSaveButton() {
const saveBtn = document.getElementById('saveVariationBtn');
if (!saveBtn) {
console.warn('🎵 Save button not found');
return;
}
if (window.currentTrackId === null) {
saveBtn.disabled = true;
saveBtn.textContent = 'No Track Selected';
return;
}
const trackCard = document.querySelector(`[data-track-id="${window.currentTrackId}"]`);
if (!trackCard) {
saveBtn.disabled = true;
saveBtn.textContent = 'Track Not Found';
return;
}
const currentSelection = trackCard.getAttribute('data-selected-variation') || '0';
if (window.selectedVariationIndex !== parseInt(currentSelection)) {
saveBtn.disabled = false;
saveBtn.textContent = 'Save Selection';
} else {
saveBtn.disabled = true;
saveBtn.textContent = 'No Changes';
}
}
// Save variation selection
function saveVariationSelection() {
if (window.selectedVariationIndex === null || window.currentTrackId === null) {
console.warn('🎵 No variation or track selected');
return;
}
console.log('🎵 Saving variation selection:', { trackId: window.currentTrackId, variationIndex: window.selectedVariationIndex });
// Disable save button during request
const saveBtn = document.getElementById('saveVariationBtn');
saveBtn.disabled = true;
saveBtn.textContent = 'Saving...';
fetch('/api_select_variation.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
track_id: window.currentTrackId,
variation_index: window.selectedVariationIndex
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('🎵 Variation selection saved:', data);
// Update the track card
const trackCard = document.querySelector(`[data-track-id="${window.currentTrackId}"]`);
if (trackCard) {
trackCard.setAttribute('data-selected-variation', window.selectedVariationIndex);
// Update the main audio URL and duration
const variation = window.trackVariations[window.selectedVariationIndex];
if (variation) {
// Update play button with signed proxy URL
const playBtn = trackCard.querySelector('.play-track-btn');
if (playBtn) {
const variationIndex = variation.variation_index !== undefined ? variation.variation_index : window.selectedVariationIndex;
// Fetch signed URL asynchronously
fetch('/api/get_audio_token.php?track_id=' + window.currentTrackId + '&variation=' + variationIndex)
.then(r => r.json())
.then(tokenData => {
if (tokenData.success && tokenData.url) {
playBtn.setAttribute('data-audio-url', tokenData.url);
}
})
.catch(err => console.error('Failed to get audio token:', err));
}
// Update duration display
const durationSpan = trackCard.querySelector('.track-details span:first-child');
if (durationSpan) {
const duration = Math.floor(variation.duration / 60) + 'm ' + Math.floor(variation.duration % 60) + 's';
durationSpan.innerHTML = `<i class="fas fa-clock"></i> ${duration}`;
}
}
}
// Show success message
if (typeof window.showNotification === 'function') {
window.showNotification('Variation selection saved successfully!', 'success');
} else {
alert('✅ Variation selection saved successfully!');
}
// Close modal
closeVariations();
} else {
console.warn('🎵 Failed to save variation selection:', data.error);
if (typeof window.showNotification === 'function') {
window.showNotification('Failed to save variation selection: ' + (data.error || 'Unknown error'), 'error');
} else {
alert('❌ Failed to save variation selection: ' + (data.error || 'Unknown error'));
}
}
})
.catch(error => {
console.warn('🎵 Error saving variation selection:', error);
if (typeof window.showNotification === 'function') {
window.showNotification('Network error saving selection. Please check your connection and try again.', 'error');
} else {
alert('❌ Network error saving selection. Please check your connection and try again.');
}
})
.finally(() => {
// Re-enable save button
saveBtn.disabled = false;
updateSaveButton();
});
}
// Close variations modal
function closeVariations() {
const modal = document.getElementById('variationsModal');
if (!modal) return;
// Remove active class
modal.classList.remove('active');
// Remove inline styles that were set in showVariations
modal.style.display = 'none';
modal.style.zIndex = '';
modal.style.position = '';
modal.style.top = '';
modal.style.left = '';
modal.style.width = '';
modal.style.height = '';
modal.style.alignItems = '';
modal.style.justifyContent = '';
// Reset variables
window.trackVariations = [];
window.currentTrackId = null;
window.selectedVariationIndex = null;
console.log('🎵 Variations modal closed');
}
// Show lyrics modal
function showLyrics(trackId) {
console.log('🎵 Showing lyrics for track:', trackId);
// Get track data
const trackCard = document.querySelector(`[data-track-id="${trackId}"]`);
if (!trackCard) {
console.warn('🎵 Track card not found for track ID:', trackId);
alert('Track not found. Please refresh the page and try again.');
return;
}
console.log('🎵 Track card found:', trackCard);
// Get track title
const titleElement = trackCard.querySelector('.track-title-mini');
const trackTitle = titleElement ? titleElement.textContent : 'Unknown Track';
console.log('🎵 Track title:', trackTitle);
// Show lyrics modal
const modal = document.getElementById('lyricsModal');
const lyricsContent = document.getElementById('lyricsContent');
console.log('🎵 Modal element:', modal);
console.log('🎵 Lyrics content element:', lyricsContent);
if (!modal || !lyricsContent) {
console.warn('🎵 Modal elements not found');
alert('Modal not found. Please refresh the page and try again.');
return;
}
// For now, show placeholder lyrics since lyrics functionality needs to be implemented
lyricsContent.innerHTML = `
<div style="text-align: center; padding: 2rem;">
<i class="fas fa-music" style="font-size: 3rem; color: #667eea; margin-bottom: 1rem;"></i>
<h3 style="color: #ffffff; margin-bottom: 1rem;">${trackTitle}</h3>
<p style="color: #a0aec0; margin-bottom: 2rem;">🎵 Lyrics feature coming soon!</p>
<div style="background: rgba(102, 126, 234, 0.1); padding: 1.5rem; border-radius: 8px; border: 1px solid rgba(102, 126, 234, 0.3);">
<p style="color: #cccccc; font-style: italic;">
"This will show the AI-generated lyrics for your track.<br>
Stay tuned for this exciting feature!"
</p>
</div>
</div>
`;
// Show modal
modal.classList.add('active');
console.log('🎵 Lyrics modal activated');
// Force modal to be visible and on top
modal.style.zIndex = '999999';
modal.style.position = 'fixed';
modal.style.top = '0';
modal.style.left = '0';
modal.style.width = '100%';
modal.style.height = '100%';
modal.style.display = 'flex';
modal.style.alignItems = 'center';
modal.style.justifyContent = 'center';
console.log('🎵 Lyrics modal styles applied:', {
zIndex: modal.style.zIndex,
position: modal.style.position,
display: modal.style.display
});
// Close on outside click
modal.addEventListener('click', function(e) {
if (e.target === this) {
closeLyrics();
}
});
}
// Close lyrics modal
function closeLyrics() {
const modal = document.getElementById('lyricsModal');
modal.classList.remove('active');
}
// Copy lyrics to clipboard
function copyLyrics() {
const lyricsContent = document.getElementById('lyricsContent');
if (lyricsContent) {
const text = lyricsContent.textContent || lyricsContent.innerText;
navigator.clipboard.writeText(text).then(() => {
alert('✅ Lyrics copied to clipboard!');
}).catch(() => {
alert('❌ Failed to copy lyrics. Please select and copy manually.');
});
}
}
// Download track
function downloadTrack(trackId) {
console.log('🎵 Downloading track:', trackId);
// Get track data
const trackCard = document.querySelector(`[data-track-id="${trackId}"]`);
if (!trackCard) {
console.warn('🎵 Track card not found for track ID:', trackId);
alert('Track not found. Please refresh the page and try again.');
return;
}
// Show download modal
const modal = document.getElementById('downloadModal');
const downloadContent = document.getElementById('downloadContent');
if (!modal || !downloadContent) {
console.warn('🎵 Download modal elements not found');
alert('Download modal not found. Please refresh the page and try again.');
return;
}
// Get track title - use the correct class name from the track card
const titleElement = trackCard.querySelector('.track-name');
const trackTitle = titleElement ? titleElement.textContent.trim() : 'Unknown Track';
// Populate download modal
downloadContent.innerHTML = `
<div style="text-align: center; padding: 2rem;">
<i class="fas fa-download" style="font-size: 3rem; color: #667eea; margin-bottom: 1rem;"></i>
<h3 style="color: #ffffff; margin-bottom: 1rem;">${trackTitle}</h3>
<p style="color: #a0aec0; margin-bottom: 2rem;">Choose your download option:</p>
<div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;">
<button onclick="downloadSingleTrack(${trackId})" style="background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; padding: 1rem 2rem; border-radius: 8px; cursor: pointer; margin: 0.5rem;">
<i class="fas fa-music"></i> Download Track
</button>
<button onclick="downloadAllVariations(${trackId})" style="background: linear-gradient(135deg, #48bb78, #38a169); color: white; border: none; padding: 1rem 2rem; border-radius: 8px; cursor: pointer; margin: 0.5rem;">
<i class="fas fa-layer-group"></i> Download All Variations
</button>
</div>
</div>
`;
// Show modal
modal.classList.add('active');
console.log('🎵 Download modal activated');
// Add click outside to close
modal.addEventListener('click', function(e) {
if (e.target === this) {
closeDownload();
}
});
// Force modal to be visible and on top
modal.style.zIndex = '999999';
modal.style.position = 'fixed';
modal.style.top = '0';
modal.style.left = '0';
modal.style.width = '100%';
modal.style.height = '100%';
modal.style.display = 'flex';
modal.style.alignItems = 'center';
modal.style.justifyContent = 'center';
}
// Close download modal
function closeDownload() {
const modal = document.getElementById('downloadModal');
if (modal) {
modal.classList.remove('active');
// Reset any forced styles
modal.style.zIndex = '';
modal.style.position = '';
modal.style.top = '';
modal.style.left = '';
modal.style.width = '';
modal.style.height = '';
modal.style.display = '';
modal.style.alignItems = '';
modal.style.justifyContent = '';
}
}
// Download single track
function downloadSingleTrack(trackId) {
const trackCard = document.querySelector(`[data-track-id="${trackId}"]`);
if (!trackCard) return;
// Get the play button which has the audio data
const playBtn = trackCard.querySelector('.action-btn-compact.primary');
if (!playBtn) {
showNotification('❌ Play button not found. Please try again.', 'error');
return;
}
const audioUrl = playBtn.getAttribute('data-audio-url');
const title = playBtn.getAttribute('data-title');
const artist = playBtn.getAttribute('data-artist');
if (!audioUrl) {
showNotification('❌ Audio URL not found. Please try again.', 'error');
return;
}
// Create download link
const link = document.createElement('a');
link.href = audioUrl;
link.download = `${artist} - ${title}.mp3`;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Show success message
showNotification('✅ Track downloaded successfully!', 'success');
closeDownload();
}
// Download all variations
function downloadAllVariations(trackId) {
const trackCard = document.querySelector(`[data-track-id="${trackId}"]`);
if (!trackCard) {
showNotification('❌ Track not found.', 'error');
return;
}
const variationsData = trackCard.getAttribute('data-variations');
if (!variationsData || variationsData === '[]') {
showNotification('❌ No variations available for this track.', 'warning');
return;
}
try {
const variations = JSON.parse(variationsData);
if (!Array.isArray(variations) || variations.length === 0) {
showNotification('❌ No variations available for this track.', 'warning');
return;
}
// Get track information from the card
const trackTitleEl = trackCard.querySelector('.track-name');
const trackTitle = trackTitleEl ? trackTitleEl.textContent.trim() : 'Untitled Track';
const artistEl = trackCard.querySelector('.track-artist-name');
const artistName = artistEl ? artistEl.textContent.replace('By ', '').trim() : '<?= htmlspecialchars($user_name) ?>';
// Get metadata from the card
const metadataRow = trackCard.querySelector('.track-metadata-row');
let genre = 'Unknown';
let bpm = '';
let key = '';
let numericalKey = '';
let mood = '';
// Try to get numerical key from prompt stored in data attribute
const trackPrompt = trackCard.getAttribute('data-prompt') || '';
if (trackPrompt) {
const keyMatch = trackPrompt.match(/key[:\s]+([0-9]+[A-G]?)\s*[–\-]?\s*/i);
if (keyMatch && keyMatch[1]) {
numericalKey = keyMatch[1].trim();
}
}
if (metadataRow) {
const metaItems = metadataRow.querySelectorAll('.meta-item');
metaItems.forEach(item => {
const text = item.textContent.trim();
if (text.includes('Genre:')) {
genre = text.replace('Genre:', '').trim();
} else if (text.includes('BPM:')) {
bpm = text.replace('BPM:', '').trim();
} else if (text.includes('Key:')) {
key = text.replace('Key:', '').trim();
} else if (text.includes('Mood:')) {
mood = text.replace('Mood:', '').trim();
}
});
}
// Sanitize filename (remove invalid characters)
function sanitizeFilename(str) {
return str.replace(/[<>:"/\\|?*]/g, '').replace(/\s+/g, ' ').trim();
}
// Download each variation with descriptive filename
variations.forEach((variation, index) => {
setTimeout(() => {
const link = document.createElement('a');
link.href = variation.audio_url;
// Create comprehensive filename with all metadata
const variationNum = index + 1;
const totalVariations = variations.length;
const cleanTitle = sanitizeFilename(trackTitle);
const cleanArtist = sanitizeFilename(artistName);
const cleanGenre = sanitizeFilename(genre);
const cleanKey = sanitizeFilename(key);
const cleanMood = sanitizeFilename(mood);
// Build filename: Artist - Title (Variation X of Y) [Genre, BPM, NumericalKey, Key, Mood].mp3
let filename = `${cleanArtist} - ${cleanTitle}`;
if (totalVariations > 1) {
filename += ` (Variation ${variationNum} of ${totalVariations})`;
}
// Add metadata to filename
const metadataParts = [];
if (cleanGenre && cleanGenre !== 'Unknown') metadataParts.push(cleanGenre);
if (bpm) metadataParts.push(`${bpm} BPM`);
// Add numerical key first if available, then the musical key
if (cleanNumericalKey) metadataParts.push(cleanNumericalKey);
if (cleanKey) metadataParts.push(cleanKey);
if (cleanMood) metadataParts.push(cleanMood);
if (metadataParts.length > 0) {
filename += ` [${metadataParts.join(', ')}]`;
}
filename += '.mp3';
link.download = filename;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}, index * 500); // Stagger downloads
});
showNotification(`✅ Downloading ${variations.length} variation(s) with full metadata!`, 'success');
closeDownload();
} catch (error) {
console.warn('Error downloading variations:', error);
showNotification('❌ Error downloading variations. Please try again.', 'error');
}
}
// Genre expansion toggle function
function toggleGenreExpansion(button, hiddenGenres) {
const trackCard = button.closest('.track-card');
const expandedGenres = trackCard.querySelector('.expanded-genres');
if (expandedGenres.style.display === 'none') {
// Show expanded genres
expandedGenres.style.display = 'flex';
button.textContent = 'Show less';
button.classList.add('expanded');
} else {
// Hide expanded genres
expandedGenres.style.display = 'none';
button.textContent = `+${hiddenGenres.length} more`;
button.classList.remove('expanded');
}
}
// Update currently playing track
function updateCurrentlyPlaying(trackId) {
// Remove currently playing class from all tracks
document.querySelectorAll('.track-card').forEach(card => {
card.classList.remove('currently-playing', 'playing');
});
// Reset all play buttons to play state
document.querySelectorAll('.play-btn-large, .play-track-btn, .action-btn.play-btn, .action-btn-compact.primary').forEach(btn => {
btn.classList.remove('playing');
const icon = btn.querySelector('i');
if (icon) {
icon.className = 'fas fa-play';
}
// Update button text if it has a span
const span = btn.querySelector('span');
if (span && span.textContent.includes('Playing')) {
span.textContent = 'Play';
}
});
// Add currently playing class to the current track and update its button
if (trackId) {
const currentCard = document.querySelector(`[data-track-id="${trackId}"]`);
if (currentCard) {
currentCard.classList.add('currently-playing', 'playing');
// Update play-btn-large button
const playBtnLarge = currentCard.querySelector('.play-btn-large');
if (playBtnLarge) {
playBtnLarge.classList.add('playing');
const icon = playBtnLarge.querySelector('i');
if (icon) {
icon.className = 'fas fa-pause';
}
}
// Update play-track-btn button
const playTrackBtn = currentCard.querySelector('.play-track-btn');
if (playTrackBtn) {
playTrackBtn.classList.add('playing');
const icon = playTrackBtn.querySelector('i');
if (icon) {
icon.className = 'fas fa-pause';
}
const span = playTrackBtn.querySelector('span');
if (span) {
span.textContent = 'Playing';
} else if (playTrackBtn.innerHTML.includes('<i')) {
playTrackBtn.innerHTML = '<i class="fas fa-pause"></i> <span>Playing</span>';
}
}
// Update compact play button
const compactPlayBtn = currentCard.querySelector('.action-btn.play-btn, .action-btn-compact.primary');
if (compactPlayBtn) {
compactPlayBtn.classList.add('playing');
const icon = compactPlayBtn.querySelector('i');
if (icon) {
icon.className = 'fas fa-pause';
}
}
}
}
}
// Build library playlist from all visible tracks
function buildLibraryPlaylist() {
const playlist = [];
document.querySelectorAll('.play-btn-large[data-track-id]').forEach(btn => {
const trackId = btn.getAttribute('data-track-id');
const audioUrl = btn.getAttribute('data-audio-url');
const title = btn.getAttribute('data-title') || 'Untitled';
const artist = btn.getAttribute('data-artist') || 'Unknown Artist';
if (trackId && audioUrl) {
playlist.push({
id: parseInt(trackId),
audio_url: audioUrl,
title: title,
artist_name: artist,
user_id: null // Library tracks belong to current user
});
}
});
console.log('🎵 Library: Built playlist with', playlist.length, 'tracks');
return playlist;
}
// Enhanced play track function with playing state AND playlist support
async function playTrackFromButton(audioUrl, title, artist) {
console.log('🎵 Library: playTrackFromButton called with:', { audioUrl, title, artist });
if (!audioUrl) {
console.warn('No audio URL provided');
return;
}
// Wait for enhanced global player to be ready
await waitForEnhancedPlayer();
// Get track ID and button element
const trackId = getTrackIdFromAudioUrl(audioUrl);
const button = document.querySelector(`[data-audio-url="${audioUrl}"]`);
const isCurrentlyPlaying = button && button.classList.contains('playing');
// Check if this track is currently playing - if so, pause it
if (isCurrentlyPlaying) {
// Check if global player is actually playing by checking the audio element
const globalPlayer = window.enhancedGlobalPlayer;
if (globalPlayer && typeof globalPlayer.togglePlayPause === 'function') {
// Get the audio element from the global player
const audioElement = document.getElementById('globalAudioElement');
if (audioElement && !audioElement.paused && audioElement.src) {
// Check if it's the same track by comparing track IDs (signed URLs have unique tokens)
const getTrackIdFromUrl = (url) => {
if (!url) return null;
const match = url.match(/[?&]id=(\d+)/);
return match ? match[1] : null;
};
const currentTrackId = getTrackIdFromUrl(audioElement.src);
const clickedTrackId = trackId || getTrackIdFromUrl(audioUrl);
if (currentTrackId && clickedTrackId && currentTrackId === clickedTrackId) {
// Same track is playing - pause it
console.log('🎵 Library: Pausing current track');
globalPlayer.togglePlayPause();
// Update button to play state
if (button) {
button.classList.remove('playing');
const icon = button.querySelector('i');
if (icon) {
icon.className = 'fas fa-play';
}
}
// Remove playing states from all tracks
document.querySelectorAll('.track-card').forEach(card => {
card.classList.remove('currently-playing', 'playing');
});
showNotification('⏸️ Paused', 'success');
return;
}
}
}
}
// Use the enhanced global player (same as community.php)
if (typeof window.enhancedGlobalPlayer !== 'undefined' && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
// BUILD AND SET THE PLAYLIST for continuous playback
const libraryPlaylist = buildLibraryPlaylist();
if (libraryPlaylist.length > 0) {
// Find the index of the current track
const clickedTrackId = parseInt(trackId) || parseInt(getTrackIdFromAudioUrl(audioUrl));
const trackIndex = libraryPlaylist.findIndex(t => t.id === clickedTrackId);
const startIndex = trackIndex !== -1 ? trackIndex : 0;
// Use loadPagePlaylist like artist_profile does - direct function that sets closure vars
if (typeof window.enhancedGlobalPlayer.loadPagePlaylist === 'function') {
window.enhancedGlobalPlayer.loadPagePlaylist(libraryPlaylist, 'library', startIndex);
console.log('🎵 Library: Playlist loaded via loadPagePlaylist, index:', startIndex, 'of', libraryPlaylist.length);
} else {
// Fallback to window storage
window._communityPlaylist = libraryPlaylist;
window._communityPlaylistType = 'library';
window._communityTrackIndex = startIndex;
console.log('🎵 Library: Playlist stored on window, index:', startIndex, 'of', libraryPlaylist.length);
}
}
// Now play the track
window.enhancedGlobalPlayer.playTrack(audioUrl, title, artist);
// Update currently playing state
console.log('🎵 Library: Track ID for playing state:', trackId);
updateCurrentlyPlaying(trackId);
console.log('🎵 Library: Track sent via enhancedGlobalPlayer -', title, '(playlist:', libraryPlaylist.length, 'tracks)');
} else {
console.error('Enhanced global player not available');
alert('Audio player not available. Please refresh the page and try again.');
}
}
// Get track ID from audio URL
function getTrackIdFromAudioUrl(audioUrl) {
// Find the button with this audio URL and get its track ID directly
const button = document.querySelector(`[data-audio-url="${audioUrl}"]`);
if (button) {
return button.getAttribute('data-track-id');
}
return null;
}
// Toggle generation parameters visibility
function toggleGenerationParams(header) {
const paramsGrid = header.nextElementSibling;
const toggleIcon = header.querySelector('.toggle-icon');
if (paramsGrid.style.display === 'none') {
paramsGrid.style.display = 'grid';
toggleIcon.classList.remove('fa-chevron-down');
toggleIcon.classList.add('fa-chevron-up');
} else {
paramsGrid.style.display = 'none';
toggleIcon.classList.remove('fa-chevron-up');
toggleIcon.classList.add('fa-chevron-down');
}
}
// Play track from waveform (like community_fixed.php)
function playTrackFromWaveform(trackId, audioUrl, title, artist) {
console.log('🎵 Library: playTrackFromWaveform called with:', { trackId, audioUrl, title, artist });
playTrackFromButton(audioUrl, title, artist);
}
// Wait for enhanced global player to be ready
function waitForEnhancedPlayer() {
return new Promise((resolve) => {
if (window.enhancedGlobalPlayer) {
resolve();
} else {
const checkPlayer = () => {
if (window.enhancedGlobalPlayer) {
resolve();
} else {
setTimeout(checkPlayer, 100);
}
};
checkPlayer();
}
});
}
// Other library functions
async function playTrackFromButtonElement(button) {
const audioUrl = button.getAttribute('data-audio-url');
const title = button.getAttribute('data-title');
const artist = button.getAttribute('data-artist');
if (!audioUrl) {
showNotification('❌ Audio URL not found. Please try again.', 'error');
return;
}
// Wait for enhanced global player to be ready
await waitForEnhancedPlayer();
// Ensure audio URL is absolute if it's relative
let finalAudioUrl = audioUrl;
if (audioUrl && !audioUrl.startsWith('http') && !audioUrl.startsWith('//')) {
if (audioUrl.startsWith('/')) {
finalAudioUrl = window.location.origin + audioUrl;
} else {
finalAudioUrl = window.location.origin + '/' + audioUrl;
}
console.log('🎵 Converted relative URL to absolute:', finalAudioUrl);
}
if (!finalAudioUrl || finalAudioUrl.trim() === '') {
console.error('❌ Audio URL is empty!');
showNotification('Audio file not available.', 'error');
return;
}
// Use the enhanced global player (same as community.php)
if (typeof window.enhancedGlobalPlayer !== 'undefined' && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
window.enhancedGlobalPlayer.playTrack(finalAudioUrl, title, artist);
console.log('🎵 Library: Track sent via enhancedGlobalPlayer -', title);
} else {
console.warn('Enhanced global player not available');
alert('Audio player not available. Please refresh the page and try again.');
}
}
// Track status monitoring system
let statusCheckInterval = null;
let processingTrackIds = [];
// Initialize status checking for processing tracks
function initStatusChecking() {
// Find all processing tracks on the page - try multiple selectors
let processingTracks = document.querySelectorAll('.track-card-modern[data-status="processing"]');
// If no tracks found, try alternative selectors
if (processingTracks.length === 0) {
processingTracks = document.querySelectorAll('[data-status="processing"]');
}
processingTrackIds = Array.from(processingTracks)
.map(track => track.getAttribute('data-track-id'))
.filter(id => id !== null && id !== '');
if (processingTrackIds.length > 0) {
console.log('🎵 Found', processingTrackIds.length, 'processing tracks, starting status monitoring...');
// Check immediately
checkAllTrackStatuses();
// Then check every 10 seconds (optimized for performance)
// Only check when page is visible
if (statusCheckInterval) {
clearInterval(statusCheckInterval);
}
statusCheckInterval = setInterval(function() {
if (!document.hidden && processingTrackIds.length > 0) {
checkAllTrackStatuses();
}
}, 10000); // Increased from 5 to 10 seconds for better performance
} else {
// No processing tracks, stop checking
if (statusCheckInterval) {
clearInterval(statusCheckInterval);
statusCheckInterval = null;
}
}
}
// Check all processing tracks
async function checkAllTrackStatuses() {
if (processingTrackIds.length === 0) {
initStatusChecking(); // Refresh the list
return;
}
console.log('🔍 Checking status for', processingTrackIds.length, 'tracks...');
// Check each processing track
for (const trackId of processingTrackIds) {
await checkTrackStatus(trackId);
// Small delay to avoid overwhelming the server
await new Promise(resolve => setTimeout(resolve, 500));
}
// Update stats after checking
updateStatsBar();
}
// Check individual track status
async function checkTrackStatus(trackId) {
try {
// Get track card first to check creation time
let trackCard = document.querySelector(`.track-card-modern[data-track-id="${trackId}"]`);
if (!trackCard) {
trackCard = document.querySelector(`[data-track-id="${trackId}"]`);
}
if (!trackCard) {
console.warn(`Track card not found for track ${trackId}`);
// Even if card not found, still check API status to update database
const response = await fetch(`/api/check_track_status.php?track_id=${trackId}&force_check=1`);
const data = await response.json();
if (data.success && data.data && data.data.status !== 'processing') {
console.log(`✅ Track ${trackId} status updated in database: ${data.data.status}`);
}
return;
}
// Check if track has been processing for more than 5 minutes
const trackCreated = trackCard.getAttribute('data-created-at');
if (trackCreated) {
const createdTime = new Date(trackCreated).getTime();
const now = Date.now();
const processingTime = (now - createdTime) / 1000 / 60; // minutes
if (processingTime > 5) {
// Track has been processing for more than 5 minutes - check API status
console.log(`⏱️ Track ${trackId} has been processing for ${processingTime.toFixed(1)} minutes - checking API status...`);
// Call API to check status
const response = await fetch(`/api/check_track_status.php?track_id=${trackId}&force_check=1`);
const data = await response.json();
if (data.success && data.data) {
const newStatus = data.data.status;
// If still processing after 5+ minutes, mark as failed
if (newStatus === 'processing') {
console.log(`⚠️ Track ${trackId} still processing after ${processingTime.toFixed(1)} minutes - marking as failed`);
// Mark as failed via API
await fetch(`/api/mark_track_failed.php?track_id=${trackId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}).catch(err => console.error('Error marking track as failed:', err));
// Update UI immediately
updateTrackCardStatus(trackCard, 'failed', {
id: trackId,
error_message: 'Track generation timed out after 5 minutes. Please try again.'
});
processingTrackIds = processingTrackIds.filter(id => id !== trackId);
return;
}
}
}
}
// Normal status check - always force check service for processing tracks
const response = await fetch(`/api/check_track_status.php?track_id=${trackId}&force_check=1`);
const data = await response.json();
if (data.success && data.data) {
const currentStatus = trackCard.getAttribute('data-status');
const newStatus = data.data.status;
// If status changed, update the UI
if (newStatus && newStatus !== currentStatus) {
console.log(`✅ Track ${trackId} status changed: ${currentStatus} → ${newStatus}`);
updateTrackCardStatus(trackCard, newStatus, data.data);
// Remove from processing list if no longer processing
if (newStatus !== 'processing') {
processingTrackIds = processingTrackIds.filter(id => id !== trackId);
}
} else if (newStatus === 'complete' && currentStatus === 'processing') {
// Force update if API says complete but UI still shows processing
console.log(`🔧 Force updating track ${trackId} from processing to complete`);
updateTrackCardStatus(trackCard, 'complete', data.data);
processingTrackIds = processingTrackIds.filter(id => id !== trackId);
} else if (newStatus === 'failed' && currentStatus === 'processing') {
// Force update if API says failed but UI still shows processing
console.log(`🔧 Force updating track ${trackId} from processing to failed`);
updateTrackCardStatus(trackCard, 'failed', data.data);
processingTrackIds = processingTrackIds.filter(id => id !== trackId);
}
}
} catch (error) {
console.error('Error checking track status:', error);
}
}
// Update track card UI when status changes
async function updateTrackCardStatus(trackCard, newStatus, trackData) {
// Update data attribute
trackCard.setAttribute('data-status', newStatus);
// Remove processing indicator if it exists (when transitioning from processing to complete)
const processingIndicator = trackCard.querySelector('.processing-indicator-modern');
if (processingIndicator) {
processingIndicator.remove();
}
// Get the actions row
const actionsRow = trackCard.querySelector('.track-actions-row');
if (!actionsRow) return;
// Clear current content
actionsRow.innerHTML = '';
if (newStatus === 'complete') {
// Get signed URL from API
let proxyAudioUrl = '';
try {
const response = await fetch('/api/get_audio_token.php?track_id=' + trackData.id);
const tokenData = await response.json();
if (tokenData.success && tokenData.url) {
proxyAudioUrl = tokenData.url;
}
} catch (err) {
console.error('Failed to get audio token:', err);
}
const title = trackData.title || 'Untitled Track';
const artist = trackCard.querySelector('.track-artist-name')?.textContent.replace('By ', '') || 'Unknown';
actionsRow.innerHTML = `
<button class="play-btn-large" onclick="playTrackFromButton('${proxyAudioUrl}', '${title.replace(/'/g, "\\'")}', '${artist.replace(/'/g, "\\'")}')"
data-audio-url="${proxyAudioUrl}"
data-title="${title.replace(/'/g, "\\'")}"
data-artist="${artist.replace(/'/g, "\\'")}"
data-track-id="${trackData.id}"
title="Play Track">
<i class="fas fa-play"></i>
</button>
<button class="action-icon-btn" onclick="shareTrack(${trackData.id})" title="<?= addslashes(t('artist_profile.share_track')) ?>">
<i class="fas fa-share-alt"></i>
</button>
<button class="action-icon-btn ${trackData.user_liked ? 'liked' : ''}" onclick="toggleLike(${trackData.id}, this)" title="<?= addslashes(t('artist_profile.like')) ?>">
<i class="fas fa-heart"></i>
</button>
<button class="action-icon-btn edit-track-btn"
data-track-id="${trackData.id}"
data-track-title="${(trackData.title || 'Untitled Track').replace(/"/g, '"')}"
data-track-prompt="${(trackData.prompt || '').replace(/"/g, '"')}"
data-track-price="${trackData.price || 0}"
data-track-public="${trackData.is_public || 0}"
title="Edit Track">
<i class="fas fa-edit"></i>
</button>
`;
// Show notification
showNotification(`🎵 "${title}" is ready to play!`, 'success');
} else if (newStatus === 'failed') {
// Show failed actions (Retry and Delete buttons)
const trackId = trackData.id || trackCard.getAttribute('data-track-id');
// Extract error message from trackData
let errorMsg = trackData.error_message || 'Track generation failed. Please try again.';
// Try to extract from metadata if error_message not directly available
if (!trackData.error_message && trackData.metadata) {
try {
const metadata = typeof trackData.metadata === 'string'
? JSON.parse(trackData.metadata)
: trackData.metadata;
const rawMsg = metadata.msg ||
metadata.error ||
metadata.error_msg ||
metadata.message ||
null;
if (rawMsg) {
// Check if this is actually a success message (not an error)
const isSuccessMessage = /\b(successfully|success|complete|done|ready|finished)\b/i.test(rawMsg);
// Only treat as error if it's NOT a success message
if (!isSuccessMessage) {
errorMsg = rawMsg;
}
}
} catch (e) {
console.warn('Could not parse metadata for error message:', e);
}
}
// Sanitize error message - ONLY remove API.Box references, keep the exact error message
errorMsg = errorMsg.replace(/\b(API\.Box|api\.box|API\.box|not found on API\.Box|not found in API\.Box|Task not found in API\.Box|Track.*not found on API\.Box)\b/gi, '');
errorMsg = errorMsg.trim();
// Clean up multiple spaces
errorMsg = errorMsg.replace(/\s+/g, ' ');
if (!errorMsg) {
errorMsg = 'Track generation failed. Please try again.';
}
actionsRow.innerHTML = `
<div class="failed-actions-modern" style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<button class="action-btn primary" onclick="retryTrack(${trackId})" title="Create a new version with the same prompt" style="padding: 0.5rem 1rem; background: #667eea; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9rem;">
<i class="fas fa-redo"></i>
Retry
</button>
<button class="action-btn danger" onclick="deleteFailedTrack(${trackId})" title="Delete this failed track" style="padding: 0.5rem 1rem; background: #ef4444; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9rem;">
<i class="fas fa-trash"></i>
Delete
</button>
</div>
`;
// Remove any existing error message div
const existingError = trackCard.querySelector('.error-message-modern');
if (existingError) {
existingError.remove();
}
// Add error message display below actions
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message-modern';
errorDiv.style.cssText = 'padding: 1rem; margin-top: 1rem; background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 8px; color: #ef4444;';
errorDiv.innerHTML = `
<div style="display: flex; align-items: start; gap: 0.5rem;">
<i class="fas fa-exclamation-triangle"></i>
<div>
<strong>Error:</strong> ${escapeHtml(errorMsg)}
</div>
</div>
`;
actionsRow.parentNode.insertBefore(errorDiv, actionsRow.nextSibling);
showNotification(`❌ Track generation failed: ${errorMsg}`, 'error');
}
// Update stats
updateStatsBar();
}
// Update stats bar with current counts
async function updateStatsBar() {
try {
const response = await fetch('/api/get_user_stats.php');
const data = await response.json();
if (data.success && data.data) {
const stats = data.data;
// Update stat numbers - be careful with selectors!
// Stat card order: 1=Total Tracks, 2=Total Minutes, 3=Credits, 4=Creator Level
const totalTracksEl = document.querySelector('.stat-card:nth-child(1) .stat-number');
// Don't update total minutes (2nd card) - it's calculated from duration
const creditsEl = document.querySelector('.stat-card:nth-child(3) .stat-number');
if (totalTracksEl) totalTracksEl.textContent = stats.total_tracks || 0;
// Update credits from API (which gets fresh from database)
if (creditsEl && stats.credits !== undefined) {
creditsEl.textContent = stats.credits;
}
// Don't update processing_tracks - that was overwriting credits before!
}
} catch (error) {
console.error('Error updating stats bar:', error);
}
}
function retryTrack(trackId) {
if (!confirm('Retry this track? This will use 1 credit and create a new version with the same prompt.')) {
return;
}
console.log('Retrying track:', trackId);
fetch(`/api/retry_track.php?track_id=${trackId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('✅ Track retry started! Status updated to processing.', 'success');
// Reload page after a short delay to show updated status
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
showNotification('❌ ' + (data.message || 'Failed to retry track'), 'error');
}
})
.catch(error => {
console.error('Error retrying track:', error);
showNotification('❌ Error retrying track. Please try again.', 'error');
});
}
function deleteFailedTrack(trackId) {
if (!confirm('Are you sure you want to delete this failed track? This action cannot be undone.')) {
return;
}
console.log('Deleting failed track:', trackId);
// Disable the button to prevent double-clicks
const deleteBtn = document.querySelector(`button[onclick*="deleteFailedTrack(${trackId})"]`);
let originalContent = null;
if (deleteBtn) {
deleteBtn.disabled = true;
originalContent = deleteBtn.innerHTML;
deleteBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Deleting...';
}
fetch(`/api/delete_track.php?track_id=${trackId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
if (data.success) {
// Clean up notified track IDs for this deleted track
if (window.trackMonitor && window.trackMonitor.notifiedTrackIds) {
window.trackMonitor.notifiedTrackIds.delete(`failed_${trackId}`);
window.trackMonitor.notifiedTrackIds.delete(`fixed_${trackId}`);
if (window.trackMonitor.saveNotifiedTrackIds) {
window.trackMonitor.saveNotifiedTrackIds();
}
console.log(`🎵 Removed track ${trackId} from notified track IDs`);
}
if (typeof showNotification === 'function') {
showNotification('✅ Track deleted successfully', 'success');
}
// Remove the track card from the page - use more specific selector
const trackCard = document.querySelector(`.track-card-modern[data-track-id="${trackId}"]`) ||
document.querySelector(`[data-track-id="${trackId}"]`) ||
document.querySelector(`.track-card[data-id="${trackId}"]`) ||
document.querySelector(`.track-item[data-track-id="${trackId}"]`);
if (trackCard) {
trackCard.style.transition = 'opacity 0.3s';
trackCard.style.opacity = '0';
setTimeout(() => {
trackCard.remove();
// Reload if no tracks left or update stats
if (typeof updateStatsBar === 'function') {
updateStatsBar();
}
}, 300);
} else {
// If we can't find the element, just reload
setTimeout(() => {
window.location.reload();
}, 1000);
}
} else {
if (typeof showNotification === 'function') {
showNotification('❌ ' + (data.message || 'Failed to delete track'), 'error');
}
// Re-enable button on error
if (deleteBtn && originalContent) {
deleteBtn.disabled = false;
deleteBtn.innerHTML = originalContent;
}
}
})
.catch(error => {
console.error('Error deleting track:', error);
if (typeof showNotification === 'function') {
showNotification('❌ Error deleting track. Please try again.', 'error');
}
// Re-enable button on error
if (deleteBtn && originalContent) {
deleteBtn.disabled = false;
deleteBtn.innerHTML = originalContent;
}
});
}
// Initialize enhanced global player integration
document.addEventListener('DOMContentLoaded', function() {
console.log('🎵 Library page loaded, initializing enhanced global player...');
// Wait for enhanced global player to be ready
waitForEnhancedPlayer().then(() => {
console.log('🎵 Enhanced global player ready for library page');
// Test enhanced global player functions
console.log('🎵 Available enhanced global player functions:', {
enhancedGlobalPlayer: typeof window.enhancedGlobalPlayer,
playTrack: typeof window.enhancedGlobalPlayer?.playTrack
});
}).catch(error => {
console.warn('🎵 Enhanced global player initialization error:', error);
});
// Initialize status checking for processing tracks
initStatusChecking();
// Enhanced filter handling
const filterSelects = document.querySelectorAll('select[name="sort"], select[name="time"], select[name="genre"], select[name="status"]');
filterSelects.forEach(select => {
select.addEventListener('change', function() {
applyFilters();
});
});
// Search input with debounce
const searchInput = document.getElementById('trackSearch');
let searchTimeout;
if (searchInput) {
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
applyFilters();
}, 500); // Wait 500ms after user stops typing
});
// Also trigger on Enter key
searchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault(); // Prevent form submission
clearTimeout(searchTimeout);
applyFilters();
}
});
// Also prevent form submit on the input
searchInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
}
});
}
// Function to apply all filters
function applyFilters() {
const url = new URL(window.location);
const params = new URLSearchParams(url.search);
// Get all filter values
const status = document.querySelector('select[name="status"]')?.value || 'all';
const sort = document.querySelector('select[name="sort"]')?.value || 'latest';
const time = document.querySelector('select[name="time"]')?.value || 'all';
const genre = document.querySelector('select[name="genre"]')?.value || '';
const search = document.getElementById('trackSearch')?.value.trim() || '';
// Update parameters
if (status !== 'all') {
params.set('status', status);
} else {
params.delete('status');
}
if (sort !== 'latest') {
params.set('sort', sort);
} else {
params.delete('sort');
}
if (time !== 'all') {
params.set('time', time);
} else {
params.delete('time');
}
if (genre) {
params.set('genre', genre);
} else {
params.delete('genre');
}
if (search) {
params.set('search', search);
} else {
params.delete('search');
}
// Reset to page 1 when filters change
params.delete('page');
// Navigate to new URL with filters
window.location.search = params.toString();
}
// Close modal when clicking outside
const modal = document.getElementById('variationsModal');
if (modal) {
modal.addEventListener('click', function(e) {
if (e.target === this) {
closeVariations();
}
});
}
// All tracks loaded at once - no pagination needed
console.log('🎵 All tracks loaded - infinite scroll not needed');
});
</script>
<!-- Track Edit Modal -->
<div id="editTrackModal" class="modal">
<div class="modal-content">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2><?= t('library.edit.title') ?></h2>
<button onclick="closeEditModal()" class="btn">
<i class="fas fa-times"></i>
</button>
</div>
<form method="POST" id="editTrackForm">
<input type="hidden" name="action" value="update_track">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(generateCSRFToken(), ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="track_id" id="editTrackId">
<div class="form-group">
<label class="form-label"><?= t('library.edit.track_title') ?></label>
<input type="text" name="title" id="editTitle" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label"><?= t('library.edit.original_prompt') ?></label>
<textarea name="description" id="editDescription" class="form-input" rows="5" placeholder="<?= htmlspecialchars(t('library.edit.prompt_placeholder')) ?>"></textarea>
</div>
<div class="form-group">
<label class="form-label"><?= t('library.edit.price') ?></label>
<select name="price" id="editPrice" class="form-input">
<option value="0.00"><?= t('library.edit.price_free') ?></option>
<option value="0.99">$0.99</option>
<option value="1.99">$1.99</option>
<option value="2.99">$2.99</option>
</select>
<small class="form-hint"><?= t('library.edit.price_hint') ?></small>
</div>
<div class="form-group">
<label class="form-label">
<input type="checkbox" name="is_public" id="editIsPublic">
<?= t('library.edit.make_public') ?>
</label>
<small class="form-hint"><?= t('library.edit.public_hint') ?></small>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary"><?= t('library.edit.update_track') ?></button>
<button type="button" onclick="closeEditModal()" class="btn"><?= t('library.edit.cancel') ?></button>
</div>
</form>
</div>
</div>
<script>
function editTrack(trackId, title, description, price, isPublic) {
const modal = document.getElementById('editTrackModal');
if (!modal) {
console.error('Edit modal not found');
alert('Edit modal not found. Please refresh the page.');
return;
}
document.getElementById('editTrackId').value = trackId;
document.getElementById('editTitle').value = title;
document.getElementById('editDescription').value = description || '';
// Set price dropdown - match to nearest tier
const priceSelect = document.getElementById('editPrice');
const priceVal = parseFloat(price) || 0;
const priceTiers = ['0.00', '0.99', '1.99', '2.99'];
let selectedTier = '0.00';
for (const tier of priceTiers) {
if (Math.abs(priceVal - parseFloat(tier)) < 0.01) {
selectedTier = tier;
break;
}
}
priceSelect.value = selectedTier;
document.getElementById('editIsPublic').checked = isPublic == 1;
// Override the !important rule by setting style directly with important
modal.style.setProperty('display', 'flex', 'important');
modal.style.setProperty('z-index', '10000', 'important');
}
function closeEditModal() {
const modal = document.getElementById('editTrackModal');
if (modal) {
modal.style.setProperty('display', 'none', 'important');
}
}
// Ensure modal is hidden on page load and set up click outside handler
document.addEventListener('DOMContentLoaded', function() {
const editModal = document.getElementById('editTrackModal');
if (editModal) {
// Force hide on page load
editModal.style.setProperty('display', 'none', 'important');
// Close modal when clicking outside
editModal.addEventListener('click', function(event) {
if (event.target === editModal) {
closeEditModal();
}
});
}
// Set up event listeners for edit buttons using data attributes
// This handles both existing buttons and dynamically added ones
function setupEditButtons() {
document.querySelectorAll('.edit-track-btn').forEach(button => {
// Remove existing listeners to avoid duplicates
const newButton = button.cloneNode(true);
button.parentNode.replaceChild(newButton, button);
newButton.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const trackId = this.getAttribute('data-track-id');
const title = this.getAttribute('data-track-title') || 'Untitled Track';
const prompt = this.getAttribute('data-track-prompt') || '';
const price = parseFloat(this.getAttribute('data-track-price')) || 0;
const isPublic = parseInt(this.getAttribute('data-track-public')) || 0;
if (trackId) {
editTrack(trackId, title, prompt, price, isPublic);
} else {
console.error('Edit button missing track ID');
alert('Error: Track ID not found. Please refresh the page.');
}
});
});
}
// Set up initial buttons
setupEditButtons();
// Re-setup buttons when track cards are dynamically updated
// Use MutationObserver to watch for new edit buttons
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes.length) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1) { // Element node
if (node.classList && node.classList.contains('edit-track-btn')) {
setupEditButtons();
} else if (node.querySelector && node.querySelector('.edit-track-btn')) {
setupEditButtons();
}
}
});
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
</script>
<!-- Create Crate Modal -->
<div id="createCrateModal" class="modal" style="display: none;">
<div class="modal-content" style="max-width: 500px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2 style="margin: 0; color: white;"><?= t('library.crates.create_modal_title') ?></h2>
<button onclick="closeCreateCrateModal()" style="background: none; border: none; color: white; font-size: 1.5rem; cursor: pointer;">
<i class="fas fa-times"></i>
</button>
</div>
<form id="createCrateForm" onsubmit="createCrate(event)">
<div style="margin-bottom: 1.5rem;">
<label style="display: block; margin-bottom: 0.5rem; color: white; font-weight: 600;"><?= t('library.crates.name_label') ?></label>
<input type="text" id="crateName" required style="width: 100%; padding: 0.75rem; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.1); color: white; font-size: 1rem;" placeholder="<?= t('library.crates.name_placeholder') ?>">
</div>
<div style="margin-bottom: 1.5rem;">
<label style="display: block; margin-bottom: 0.5rem; color: white; font-weight: 600;"><?= t('library.crates.description_label') ?></label>
<textarea id="crateDescription" rows="3" style="width: 100%; padding: 0.75rem; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.1); color: white; font-size: 1rem; resize: vertical;" placeholder="<?= t('library.crates.description_placeholder') ?>"></textarea>
</div>
<div style="display: flex; gap: 1rem; justify-content: flex-end;">
<button type="button" onclick="closeCreateCrateModal()" style="padding: 0.75rem 1.5rem; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.1); color: white; cursor: pointer; font-weight: 600;">Cancel</button>
<button type="submit" style="padding: 0.75rem 1.5rem; border-radius: 8px; border: none; background: linear-gradient(135deg, #667eea, #764ba2); color: white; cursor: pointer; font-weight: 600;">Create Crate</button>
</div>
</form>
</div>
</div>
<!-- Rename Crate Modal -->
<div id="renameCrateModal" class="modal" style="display: none;">
<div class="modal-content" style="max-width: 450px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2 style="margin: 0; color: white;"><?= t('library.crates.rename_modal_title') ?></h2>
<button onclick="closeRenameCrateModal()" style="background: none; border: none; color: white; font-size: 1.5rem; cursor: pointer;">
<i class="fas fa-times"></i>
</button>
</div>
<form id="renameCrateForm" onsubmit="renameCrate(event)">
<input type="hidden" id="renameCrateId" value="">
<div style="margin-bottom: 1.5rem;">
<label style="display: block; margin-bottom: 0.5rem; color: white; font-weight: 600;"><?= t('library.crates.new_name') ?></label>
<input type="text" id="newCrateName" required style="width: 100%; padding: 0.75rem; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.1); color: white; font-size: 1rem;" placeholder="<?= t('library.crates.name_placeholder') ?>">
</div>
<div style="display: flex; gap: 1rem; justify-content: flex-end;">
<button type="button" onclick="closeRenameCrateModal()" style="padding: 0.75rem 1.5rem; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.1); color: white; cursor: pointer; font-weight: 600;">Cancel</button>
<button type="submit" style="padding: 0.75rem 1.5rem; border-radius: 8px; border: none; background: linear-gradient(135deg, #fbbf24, #f59e0b); color: #1a1a2e; cursor: pointer; font-weight: 600;"><i class="fas fa-pencil-alt"></i> <?= t('library.crates.rename') ?></button>
</div>
</form>
</div>
</div>
<!-- View Crate Modal - Draggable & Resizable -->
<div id="viewCrateModal" class="modal crate-floating-modal" style="display: none;">
<div id="crateModalContent" class="modal-content crate-modal-draggable" style="width: 500px; height: 400px; min-width: 350px; min-height: 250px; display: flex; flex-direction: column; overflow: hidden; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); resize: both;">
<!-- Draggable Header -->
<div id="crateModalHeader" class="crate-modal-header" style="display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 1rem; background: rgba(102, 126, 234, 0.3); border-radius: 12px 12px 0 0; cursor: move; flex-shrink: 0; user-select: none;">
<div style="display: flex; align-items: center; gap: 0.5rem; flex: 1; min-width: 0;">
<i class="fas fa-grip-vertical" style="color: #a0aec0; font-size: 0.9rem;"></i>
<h2 id="crateModalTitle" style="margin: 0; color: white; font-size: 1.1rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"><?= t('library.crates.details') ?></h2>
</div>
<div style="display: flex; gap: 0.25rem; flex-shrink: 0; align-items: center;">
<!-- BPM Sort -->
<select id="crateBPMSort" onchange="sortCrateTracks('bpm', this.value)" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; padding: 0.4rem 0.5rem; border-radius: 6px; cursor: pointer; font-size: 0.8rem; font-weight: 500;" title="Sort by BPM">
<option value="">BPM</option>
<option value="asc">BPM ↑</option>
<option value="desc">BPM ↓</option>
</select>
<!-- Key Sort -->
<select id="crateKeySort" onchange="sortCrateTracks('key', this.value)" style="background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; padding: 0.4rem 0.5rem; border-radius: 6px; cursor: pointer; font-size: 0.8rem; font-weight: 500;" title="Sort by Key">
<option value="">Key</option>
<option value="asc">Key ↑ (1A→12B)</option>
<option value="desc">Key ↓ (12B→1A)</option>
<option value="harmonic">Harmonic Journey</option>
</select>
<button id="playCrateBtn" onclick="window.playCrateAsPlaylist()" style="background: linear-gradient(135deg, #10b981, #059669); color: white; border: none; padding: 0.4rem 0.75rem; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.85rem; display: flex; align-items: center; gap: 0.3rem;" title="<?= t('library.crates.play_all') ?>">
<i class="fas fa-play"></i> <?= t('library.crates.play_all') ?>
</button>
<button id="downloadCrateBtn" onclick="window.downloadCrateAsZip()" style="background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; padding: 0.4rem 0.75rem; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 0.85rem; display: flex; align-items: center; gap: 0.3rem;" title="<?= t('library.crates.download_all') ?>">
<i class="fas fa-download"></i> <?= t('library.crates.download_all') ?>
</button>
<button onclick="closeViewCrateModal()" style="background: rgba(255,255,255,0.1); border: none; color: white; font-size: 1.1rem; cursor: pointer; padding: 0.3rem 0.5rem; border-radius: 6px;">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<!-- Set Info -->
<div id="crateSetInfo" style="padding: 0.5rem 1rem; color: #a0aec0; font-size: 0.85rem; background: rgba(0,0,0,0.2); flex-shrink: 0;"></div>
<!-- Crate Description (Editable) -->
<div id="crateDescriptionSection" style="padding: 0.5rem 1rem; background: rgba(0,0,0,0.1); border-top: 1px solid rgba(255,255,255,0.05); flex-shrink: 0;">
<div style="display: flex; align-items: flex-start; gap: 0.5rem;">
<div style="flex: 1; display: flex; flex-direction: column; gap: 0.25rem;">
<textarea id="crateDescriptionInput" placeholder="<?= t('library.crates.add_description') ?>" style="width: 100%; min-height: 40px; max-height: 80px; padding: 0.5rem; border-radius: 6px; border: 1px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.05); color: #e0e0e0; font-size: 0.85rem; resize: vertical; font-family: inherit;"></textarea>
<div id="descriptionSaveStatus" style="font-size: 0.7rem; color: #a0aec0; display: none;"></div>
</div>
<div style="display: flex; flex-direction: column; gap: 0.25rem;">
<button id="saveDescriptionBtn" onclick="saveCrateDescription()" style="background: rgba(16, 185, 129, 0.2); border: 1px solid rgba(16, 185, 129, 0.4); color: #10b981; padding: 0.35rem 0.5rem; border-radius: 4px; cursor: pointer; font-size: 0.75rem; display: flex; align-items: center; gap: 0.25rem; transition: all 0.2s;" title="<?= t('library.crates.save_description') ?>">
<i class="fas fa-save"></i>
</button>
<button id="toggleDescriptionVisibilityBtn" onclick="toggleCrateDescriptionVisibility()" style="background: none; border: 1px solid rgba(255,255,255,0.2); color: #a0aec0; padding: 0.35rem 0.5rem; border-radius: 4px; cursor: pointer; font-size: 0.75rem; display: flex; align-items: center; gap: 0.25rem; transition: all 0.2s;" title="">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
</div>
<!-- Tracks List -->
<div id="crateTracksList" style="display: flex; flex-direction: column; gap: 0.75rem; flex: 1; overflow-y: auto; padding: 0.75rem;">
<!-- Tracks will be loaded here -->
</div>
<!-- Resize Handle Indicator -->
<div style="position: absolute; bottom: 2px; right: 2px; width: 12px; height: 12px; cursor: nwse-resize; opacity: 0.5;">
<i class="fas fa-grip-lines" style="transform: rotate(-45deg); font-size: 0.7rem; color: #a0aec0;"></i>
</div>
</div>
</div>
<style>
/* Floating Crate Modal Styles */
.crate-floating-modal {
background: transparent !important;
pointer-events: none;
}
.crate-floating-modal .crate-modal-draggable {
pointer-events: auto;
background: rgba(26, 26, 26, 0.98);
backdrop-filter: blur(20px);
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.crate-modal-header:hover {
background: rgba(102, 126, 234, 0.4);
}
.crate-modal-draggable::-webkit-resizer {
background: transparent;
}
</style>
<!-- Add to Crate Modal -->
<div id="addToCrateModal" class="modal" style="display: none;">
<div class="modal-content" style="max-width: 400px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2 style="margin: 0; color: white;"><?= t('library.crates.add_track') ?></h2>
<button onclick="closeAddToCrateModal()" style="background: none; border: none; color: white; font-size: 1.5rem; cursor: pointer;">
<i class="fas fa-times"></i>
</button>
</div>
<div id="cratesSelectList" style="display: flex; flex-direction: column; gap: 0.5rem; max-height: 400px; overflow-y: auto;">
<!-- Crates will be loaded here -->
</div>
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid rgba(255, 255, 255, 0.1);">
<button onclick="showCreateCrateModal()" style="width: 100%; padding: 0.75rem; border-radius: 8px; border: 1px dashed rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.05); color: white; cursor: pointer; font-weight: 600;">
<i class="fas fa-plus"></i> <?= t('library.crates.create_new') ?>
</button>
</div>
</div>
</div>
<script>
// Crate Management Functions
let currentTrackIdForCrate = null;
let crates = [];
// Load crates on page load
document.addEventListener('DOMContentLoaded', function() {
loadCrates();
});
function loadCrates() {
fetch('/api/get_user_crates.php')
.then(response => response.json())
.then(data => {
if (data.success) {
crates = data.crates;
renderCrates();
} else {
console.error('Error loading crates:', data.error);
document.getElementById('cratesList').innerHTML = `<div style="text-align: center; padding: 2rem; color: #a0aec0;">${libraryTranslations.crates_error_loading}</div>`;
}
})
.catch(error => {
console.error('Error loading crates:', error);
document.getElementById('cratesList').innerHTML = '<div style="text-align: center; padding: 2rem; color: #a0aec0;">Error loading crates. Please refresh the page.</div>';
});
}
function renderCrates() {
const cratesList = document.getElementById('cratesList');
if (crates.length === 0) {
cratesList.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; padding: 3rem; color: #a0aec0;">
<div style="font-size: 4rem; margin-bottom: 1rem; opacity: 0.5;">📦</div>
<h4 style="font-size: 1.6rem; color: white; margin-bottom: 0.5rem;">${libraryTranslations.crates_empty_title}</h4>
<p style="font-size: 1.2rem; margin-bottom: 1.5rem;">${libraryTranslations.crates_empty_desc}</p>
<button onclick="showCreateCrateModal()" style="background: linear-gradient(135deg, #667eea, #764ba2); color: white; border: none; padding: 1rem 2rem; border-radius: 12px; font-weight: 600; cursor: pointer;">
<i class="fas fa-plus"></i> ${libraryTranslations.crates_create_first}
</button>
</div>
`;
return;
}
cratesList.innerHTML = crates.map(crate => {
const setDuration = crate.set_duration_minutes || 0;
const is2HourSet = crate.is_2_hour_set || false;
const progressPercent = crate.set_progress_percent || 0;
const tracksNeeded = crate.tracks_needed_for_2h || 0;
const isPublic = crate.is_public === true || crate.is_public === 1 || crate.is_public === '1';
return `
<div style="background: rgba(255, 255, 255, 0.05); padding: 1.5rem; border-radius: 15px; border: 1px solid ${isPublic ? 'rgba(102, 126, 234, 0.3)' : 'rgba(255, 255, 255, 0.1)'}; transition: all 0.3s ease; cursor: pointer; position: relative;"
onmouseover="this.style.background='rgba(255, 255, 255, 0.08)'"
onmouseout="this.style.background='rgba(255, 255, 255, 0.05)'"
onclick="viewCrate(${crate.id})"
data-crate-id="${crate.id}">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
<div style="flex: 1; min-width: 0;">
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;">
<h4 style="font-size: 1.4rem; font-weight: 700; color: white; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;" title="${escapeHtml(crate.name)}">${escapeHtml(crate.name)}</h4>
${isPublic ? '<span style="background: linear-gradient(135deg, #667eea, #764ba2); color: white; font-size: 0.65rem; padding: 0.2rem 0.5rem; border-radius: 10px; font-weight: 600;">PUBLIC</span>' : ''}
</div>
${crate.description ? `<p style="font-size: 1rem; color: #a0aec0; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;" title="${escapeHtml(crate.description)}">${escapeHtml(crate.description)}</p>` : ''}
</div>
<div style="display: flex; gap: 0.4rem; flex-shrink: 0;">
<button onclick="event.stopPropagation(); toggleCrateVisibility(${crate.id}, this)"
style="background: ${isPublic ? 'rgba(102, 126, 234, 0.25)' : 'rgba(255, 255, 255, 0.1)'}; color: ${isPublic ? '#667eea' : '#a0aec0'}; border: 1px solid ${isPublic ? 'rgba(102, 126, 234, 0.4)' : 'rgba(255, 255, 255, 0.2)'}; padding: 0.5rem; border-radius: 6px; cursor: pointer; font-size: 0.9rem; transition: all 0.2s ease;"
title="${isPublic ? libraryTranslations.crates_visibility_public : libraryTranslations.crates_visibility_private}"
data-is-public="${isPublic}">
<i class="fas ${isPublic ? 'fa-box-open' : 'fa-box'}"></i>
</button>
<button onclick="event.stopPropagation(); shareCrate(${crate.id}, '${escapeHtml(crate.name)}')"
style="background: rgba(16, 185, 129, 0.15); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.3); padding: 0.5rem; border-radius: 6px; cursor: pointer; font-size: 0.9rem; transition: all 0.2s ease;"
title="${libraryTranslations.crates_share || 'Share crate'}">
<i class="fas fa-share-alt"></i>
</button>
<button onclick="event.stopPropagation(); showRenameCrateModal(${crate.id}, '${escapeHtml(crate.name).replace(/'/g, "\\'")}')"
style="background: rgba(251, 191, 36, 0.15); color: #fbbf24; border: 1px solid rgba(251, 191, 36, 0.3); padding: 0.5rem; border-radius: 6px; cursor: pointer; font-size: 0.9rem; transition: all 0.2s ease;"
title="${libraryTranslations.crates_rename || 'Rename'}">
<i class="fas fa-pencil-alt"></i>
</button>
${!crate.is_primary ? `<button onclick="event.stopPropagation(); deleteCrate(${crate.id})" style="background: rgba(239, 68, 68, 0.2); color: #ef4444; border: none; padding: 0.5rem; border-radius: 6px; cursor: pointer; font-size: 0.9rem;" title="Delete crate"><i class="fas fa-trash"></i></button>` : ''}
</div>
</div>
<div style="margin-bottom: 1rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
<span style="font-size: 1rem; color: #a0aec0;">${libraryTranslations.crates_set_duration}</span>
<span style="font-size: 1.1rem; font-weight: 600; color: ${is2HourSet ? '#10b981' : '#f59e0b'};">
${setDuration.toFixed(1)} ${libraryTranslations.crates_min_set} ${is2HourSet ? '✓' : ''}
</span>
</div>
<div style="width: 100%; height: 6px; background: rgba(255, 255, 255, 0.1); border-radius: 3px; overflow: hidden;">
<div style="height: 100%; background: ${is2HourSet ? 'linear-gradient(90deg, #10b981, #059669)' : 'linear-gradient(90deg, #f59e0b, #d97706)'}; width: ${Math.min(100, progressPercent)}%; transition: width 0.3s ease;"></div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 0.5rem; font-size: 0.9rem; color: #a0aec0;">
<span>${crate.track_count || 0} ${libraryTranslations.crates_tracks}</span>
${!is2HourSet ? `<span>${tracksNeeded} ${libraryTranslations.crates_tracks_needed}</span>` : `<span style="color: #10b981;">✓ ${libraryTranslations.crates_ready}</span>`}
</div>
</div>
</div>
`;
}).join('');
}
function showCreateCrateModal() {
const modal = document.getElementById('createCrateModal');
if (modal) {
modal.style.setProperty('display', 'flex', 'important');
}
const nameInput = document.getElementById('crateName');
const descInput = document.getElementById('crateDescription');
if (nameInput) nameInput.value = '';
if (descInput) descInput.value = '';
}
function closeCreateCrateModal() {
const modal = document.getElementById('createCrateModal');
if (modal) {
modal.style.setProperty('display', 'none', 'important');
}
}
function createCrate(event) {
event.preventDefault();
const name = document.getElementById('crateName').value.trim();
const description = document.getElementById('crateDescription').value.trim();
if (!name) {
alert(libraryTranslations.crates_name_required);
return;
}
fetch('/api/create_crate.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: name,
description: description
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
closeCreateCrateModal();
loadCrates();
if (currentTrackIdForCrate) {
addTrackToCrate(data.crate_id, currentTrackIdForCrate);
currentTrackIdForCrate = null;
}
} else {
alert('Error creating crate: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error creating crate:', error);
alert('Error creating crate. Please try again.');
});
}
// Rename crate modal functions
function showRenameCrateModal(crateId, currentName) {
const modal = document.getElementById('renameCrateModal');
if (modal) {
modal.style.setProperty('display', 'flex', 'important');
}
document.getElementById('renameCrateId').value = crateId;
const nameInput = document.getElementById('newCrateName');
if (nameInput) {
nameInput.value = currentName;
nameInput.focus();
nameInput.select();
}
}
function closeRenameCrateModal() {
const modal = document.getElementById('renameCrateModal');
if (modal) {
modal.style.setProperty('display', 'none', 'important');
}
document.getElementById('renameCrateId').value = '';
document.getElementById('newCrateName').value = '';
}
function renameCrate(event) {
event.preventDefault();
const crateId = document.getElementById('renameCrateId').value;
const newName = document.getElementById('newCrateName').value.trim();
if (!newName) {
alert(libraryTranslations.crates_name_required);
return;
}
fetch('/api/update_crate_name.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
crate_id: parseInt(crateId),
name: newName
})
})
.then(response => {
if (!response.ok) {
throw new Error('HTTP error ' + response.status);
}
return response.json();
})
.then(data => {
if (data.success) {
closeRenameCrateModal();
loadCrates();
if (typeof showNotification === 'function') {
showNotification(libraryTranslations.crates_renamed || 'Crate renamed successfully', 'success');
}
// Also update the view modal title if it's open for this crate
if (currentCrateId == crateId) {
const modalTitle = document.getElementById('crateModalTitle');
if (modalTitle) {
modalTitle.textContent = newName;
}
}
} else {
alert('Error: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error renaming crate:', error);
alert('Error: ' + error.message);
});
}
// Toggle crate visibility (public/private)
function toggleCrateVisibility(crateId, button) {
const icon = button.querySelector('i');
const originalIcon = icon.className;
icon.className = 'fas fa-spinner fa-spin';
button.disabled = true;
fetch('/api/toggle_crate_visibility.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ crate_id: crateId })
})
.then(response => response.json())
.then(data => {
button.disabled = false;
if (data.success) {
const isPublic = data.is_public;
button.setAttribute('data-is-public', isPublic);
button.style.background = isPublic ? 'rgba(102, 126, 234, 0.25)' : 'rgba(255, 255, 255, 0.1)';
button.style.color = isPublic ? '#667eea' : '#a0aec0';
button.style.borderColor = isPublic ? 'rgba(102, 126, 234, 0.4)' : 'rgba(255, 255, 255, 0.2)';
button.title = isPublic ? (libraryTranslations.crates_visibility_public || 'Public - visible on profile') : (libraryTranslations.crates_visibility_private || 'Private - only you can see');
icon.className = isPublic ? 'fas fa-box-open' : 'fas fa-box';
// Update the crate card border
const crateCard = button.closest('[data-crate-id]');
if (crateCard) {
crateCard.style.borderColor = isPublic ? 'rgba(102, 126, 234, 0.3)' : 'rgba(255, 255, 255, 0.1)';
// Update or remove PUBLIC badge
const titleContainer = crateCard.querySelector('h4').parentElement;
let badge = titleContainer.querySelector('.public-badge');
if (isPublic && !badge) {
const newBadge = document.createElement('span');
newBadge.className = 'public-badge';
newBadge.style.cssText = 'background: linear-gradient(135deg, #667eea, #764ba2); color: white; font-size: 0.65rem; padding: 0.2rem 0.5rem; border-radius: 10px; font-weight: 600;';
newBadge.textContent = 'PUBLIC';
titleContainer.appendChild(newBadge);
} else if (!isPublic && badge) {
badge.remove();
}
}
// Update crates array
const crateIndex = crates.findIndex(c => c.id == crateId);
if (crateIndex !== -1) {
crates[crateIndex].is_public = isPublic;
}
// If this crate is currently open in the modal, update the description toggle button
if (window.currentCrateData && window.currentCrateData.id == crateId) {
window.currentCrateIsPublic = isPublic;
window.currentCrateData.is_public = isPublic;
// Update the description toggle button state based on new crate visibility
updateDescriptionToggleButton(window.currentCrateDescriptionPublic, isPublic);
}
showCrateNotification(data.message, 'success');
} else {
icon.className = originalIcon;
showCrateNotification(data.error || 'Failed to update visibility', 'error');
}
})
.catch(error => {
button.disabled = false;
icon.className = originalIcon;
console.error('Error toggling crate visibility:', error);
showCrateNotification('Error updating visibility. Please try again.', 'error');
});
}
// Share crate - copy link to clipboard
function shareCrate(crateId, crateName) {
const shareUrl = `${window.location.origin}/crate/${crateId}`;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(shareUrl)
.then(() => {
showCrateNotification(libraryTranslations.crates_link_copied || `Link to "${crateName}" copied to clipboard!`, 'success');
})
.catch(err => {
console.error('Clipboard error:', err);
fallbackCopyToClipboard(shareUrl, crateName);
});
} else {
fallbackCopyToClipboard(shareUrl, crateName);
}
}
function fallbackCopyToClipboard(text, crateName) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
showCrateNotification(libraryTranslations.crates_link_copied || `Link to "${crateName}" copied!`, 'success');
} catch (err) {
showCrateNotification('Failed to copy link', 'error');
}
document.body.removeChild(textarea);
}
// Show notification for crate actions
function showCrateNotification(message, type = 'info') {
if (typeof showNotification === 'function') {
showNotification(message, type);
} else {
// Fallback notification
const notif = document.createElement('div');
notif.style.cssText = `position: fixed; bottom: 20px; right: 20px; padding: 1rem 1.5rem; border-radius: 10px; color: white; font-weight: 500; z-index: 99999; animation: slideIn 0.3s ease; background: ${type === 'success' ? 'linear-gradient(135deg, #10b981, #059669)' : type === 'error' ? 'linear-gradient(135deg, #ef4444, #dc2626)' : 'linear-gradient(135deg, #667eea, #764ba2)'};`;
notif.textContent = message;
document.body.appendChild(notif);
setTimeout(() => {
notif.style.animation = 'fadeOut 0.3s ease';
setTimeout(() => notif.remove(), 300);
}, 3000);
}
}
function viewCrate(crateId) {
const modal = document.getElementById('viewCrateModal');
const tracksList = document.getElementById('crateTracksList');
// Show modal immediately with loading state
modal.style.setProperty('display', 'flex', 'important');
document.getElementById('crateModalTitle').textContent = libraryTranslations.crates_loading;
tracksList.innerHTML = `<div style="text-align: center; padding: 2rem; color: #a0aec0;"><i class="fas fa-spinner fa-spin" style="font-size: 1.5rem; margin-bottom: 0.5rem;"></i><div>${libraryTranslations.crates_loading_tracks}</div></div>`;
fetch(`/api/get_crate_tracks.php?crate_id=${crateId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text().then(text => {
try {
return JSON.parse(text);
} catch (e) {
console.error('Invalid JSON response:', text);
throw new Error('Invalid JSON response from server');
}
});
})
.then(data => {
console.log('Crate data received:', data);
if (data.success) {
document.getElementById('crateModalTitle').textContent = data.crate.name || libraryTranslations.crates_details;
// Show description section (always visible for editing)
const descInput = document.getElementById('crateDescriptionInput');
const descStatus = document.getElementById('descriptionSaveStatus');
descInput.value = data.crate.description || '';
descStatus.style.display = 'none';
// Store crate public status
const crateIsPublic = (data.crate.is_public == 1 || data.crate.is_public === true);
window.currentCrateIsPublic = crateIsPublic;
// Properly handle is_description_public:
// - If null/undefined: default to true (public)
// - If 0: false (private)
// - If 1 or true: true (public)
const descPublicValue = data.crate.is_description_public;
if (descPublicValue === null || descPublicValue === undefined) {
window.currentCrateDescriptionPublic = true; // Default to public
} else {
window.currentCrateDescriptionPublic = (descPublicValue == 1 || descPublicValue === true);
}
console.log('Crate description visibility initialized:', {
crateIsPublic: crateIsPublic,
raw: descPublicValue,
calculated: window.currentCrateDescriptionPublic
});
updateDescriptionToggleButton(window.currentCrateDescriptionPublic, crateIsPublic);
const setDuration = data.total_set_duration_minutes || 0;
const is2HourSet = data.is_2_hour_set || false;
const progressPercent = data.set_progress_percent || 0;
document.getElementById('crateSetInfo').innerHTML = `
<div style="display: flex; align-items: center; gap: 1rem;">
<span style="color: ${is2HourSet ? '#10b981' : '#f59e0b'}; font-weight: 600;">
${setDuration.toFixed(1)} ${libraryTranslations.crates_min_set} / 120 ${libraryTranslations.crates_min_set}
</span>
<div style="flex: 1; height: 4px; background: rgba(255, 255, 255, 0.1); border-radius: 2px; overflow: hidden;">
<div style="height: 100%; background: ${is2HourSet ? 'linear-gradient(90deg, #10b981, #059669)' : 'linear-gradient(90deg, #f59e0b, #d97706)'}; width: ${Math.min(100, progressPercent)}%;"></div>
</div>
${is2HourSet ? `<span style="color: #10b981;">✓ ${libraryTranslations.crates_ready_short}</span>` : `<span style="color: #a0aec0;">${Math.ceil((120 - setDuration) / 2.5)} ${libraryTranslations.crates_tracks_needed}</span>`}
</div>
`;
if (!data.tracks || data.tracks.length === 0) {
tracksList.innerHTML = `<div style="text-align: center; padding: 2rem; color: #a0aec0;"><div style="font-size: 2rem; margin-bottom: 0.5rem;">📦</div><div>${libraryTranslations.crates_no_tracks}</div><div style="margin-top: 1rem; font-size: 0.9rem;">${libraryTranslations.crates_add_tracks_desc}</div></div>`;
} else {
// Store crate data for reordering
window.currentCrateData = {
id: crateId,
tracks: data.tracks,
name: data.crate.name,
is_public: window.currentCrateIsPublic,
is_description_public: window.currentCrateDescriptionPublic
};
// Helper function to escape HTML attributes
const escapeHtmlAttr = (str) => {
if (!str) return '';
return String(str)
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '<')
.replace(/>/g, '>');
};
// Parse metadata for BPM and Key
data.tracks.forEach(track => {
if (track.metadata) {
try {
const metadata = typeof track.metadata === 'string' ? JSON.parse(track.metadata) : track.metadata;
track.bpm = metadata.bpm || null;
track.key = metadata.key || null;
track.camelot = metadata.numerical_key || null;
} catch (e) {
track.bpm = null;
track.key = null;
track.camelot = null;
}
} else {
track.bpm = null;
track.key = null;
track.camelot = null;
}
});
tracksList.innerHTML = data.tracks.map((track, index) => {
const trackDuration = parseFloat(track.duration) || 0;
const setDuration = parseFloat(track.set_duration_minutes) || (trackDuration >= 300 ? 2.5 : trackDuration * 0.5 / 60);
const minutes = Math.floor(trackDuration / 60);
const seconds = Math.floor(trackDuration % 60);
const position = track.position || (index + 1);
// Format BPM and Key for display
const bpmDisplay = track.bpm ? track.bpm.toFixed(1) : '—';
const keyDisplay = track.key ? track.key : '—';
const camelotDisplay = track.camelot ? track.camelot : '';
return `
<div class="crate-track-item"
data-track-id="${track.id}"
data-position="${position}"
data-bpm="${track.bpm || ''}"
data-camelot="${track.camelot || ''}"
style="display: flex; align-items: center; gap: 1rem; padding: 1rem; background: rgba(255, 255, 255, 0.03); border-radius: 10px; border: 1px solid rgba(255, 255, 255, 0.1); transition: all 0.3s ease; cursor: move;"
onmouseover="this.style.background='rgba(255, 255, 255, 0.06)'"
onmouseout="this.style.background='rgba(255, 255, 255, 0.03)'"
draggable="true"
ondragstart="handleDragStart(event, ${track.id})"
ondragover="handleDragOver(event)"
ondrop="handleDrop(event, ${track.id}, ${crateId})"
ondragend="handleDragEnd(event)">
<div style="display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0;">
<div class="drag-handle" style="color: #a0aec0; cursor: grab; padding: 0.25rem;" title="${libraryTranslations.crates_drag_reorder}">
<i class="fas fa-grip-vertical"></i>
</div>
<div style="width: 2.5rem; height: 2.5rem; border-radius: 8px; background: linear-gradient(135deg, #667eea, #764ba2); display: flex; align-items: center; justify-content: center; color: white; font-weight: 600; font-size: 0.9rem;">
${position}
</div>
</div>
<div style="flex: 1; min-width: 0;">
<div style="font-weight: 600; color: white; margin-bottom: 0.25rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${escapeHtml(track.title || libraryTranslations.untitled_track)}</div>
<div style="font-size: 0.9rem; color: #a0aec0; display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap;">
<span>${minutes}:${String(seconds).padStart(2, '0')}</span>
<span>•</span>
<span><strong>BPM:</strong> ${bpmDisplay}</span>
<span>•</span>
<span><strong>Key:</strong> ${keyDisplay}${camelotDisplay ? ' <span style="color: #667eea; font-weight: 600;">(' + camelotDisplay + ')</span>' : ''}</span>
<span>•</span>
<span>${libraryTranslations.crates_set_duration}: ${setDuration.toFixed(1)} ${libraryTranslations.crates_min_set}</span>
</div>
</div>
<div style="display: flex; gap: 0.5rem; flex-shrink: 0;">
<button onclick="event.stopPropagation(); window.playCrateFromTrack(${index})"
style="background: rgba(16, 185, 129, 0.2); color: #10b981; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.9rem; transition: all 0.3s ease;"
onmouseover="this.style.background='rgba(16, 185, 129, 0.3)'"
onmouseout="this.style.background='rgba(16, 185, 129, 0.2)'"
title="Play from here">
<i class="fas fa-play"></i>
</button>
<button onclick="toggleCrateTrackVisibility(${crateId}, ${track.id}, this)"
style="background: ${track.crate_track_public == 1 || track.crate_track_public === undefined ? 'rgba(102, 126, 234, 0.25)' : 'rgba(107, 114, 128, 0.25)'}; color: ${track.crate_track_public == 1 || track.crate_track_public === undefined ? '#667eea' : '#6b7280'}; border: 1px solid ${track.crate_track_public == 1 || track.crate_track_public === undefined ? 'rgba(102, 126, 234, 0.4)' : 'rgba(107, 114, 128, 0.4)'}; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.9rem; transition: all 0.3s ease;"
data-is-public="${track.crate_track_public == 1 || track.crate_track_public === undefined ? '1' : '0'}"
title="${track.crate_track_public == 1 || track.crate_track_public === undefined ? (libraryTranslations.crates_track_public || 'Track visible in public crate') : (libraryTranslations.crates_track_private || 'Track hidden from public crate')}">
<i class="fas ${track.crate_track_public == 1 || track.crate_track_public === undefined ? 'fa-eye' : 'fa-eye-slash'}"></i>
</button>
${(track.user_owns_track == 1 || track.user_purchased_track == 1) ? `
<button onclick="window.downloadCrateTrack(${track.id}, ${crateId}, ${position})"
style="background: rgba(102, 126, 234, 0.2); color: #667eea; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.9rem; transition: all 0.3s ease;"
onmouseover="this.style.background='rgba(102, 126, 234, 0.3)'"
onmouseout="this.style.background='rgba(102, 126, 234, 0.2)'"
title="${libraryTranslations.crates_download}: ${escapeHtml(data.crate.name)} - Track ${position}">
<i class="fas fa-download"></i>
</button>
` : `
<button onclick="event.stopPropagation(); window.addToCartFromCrate(${track.id}, this)"
data-track-id="${track.id}"
data-track-title="${escapeHtml(track.title || 'Untitled Track')}"
data-track-price="${track.price || 0}"
class="btn-cart"
style="background: rgba(245, 158, 11, 0.2); color: #f59e0b; border: none; padding: 0.5rem; border-radius: 6px; cursor: pointer; font-size: 0.85rem; transition: all 0.3s ease; min-width: 36px; display: flex; align-items: center; justify-content: center;"
onmouseover="this.style.background='rgba(245, 158, 11, 0.3)'"
onmouseout="this.style.background='rgba(245, 158, 11, 0.2)'"
title="${libraryTranslations.crates_add_to_cart || 'Add to Cart'}${track.price && track.price > 0 ? ' ($' + track.price + ')' : ' (' + (libraryTranslations.free || 'Free') + ')'}">
<i class="fas fa-shopping-cart"></i>
</button>
`}
<button onclick="removeTrackFromCrate(${crateId}, ${track.id})"
style="background: rgba(239, 68, 68, 0.2); color: #ef4444; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.9rem; transition: all 0.3s ease;"
onmouseover="this.style.background='rgba(239, 68, 68, 0.3)'"
onmouseout="this.style.background='rgba(239, 68, 68, 0.2)'"
title="${libraryTranslations.crates_remove}">
<i class="fas fa-times"></i>
</button>
</div>
</div>
`;
}).join('');
// Initialize drag and drop
initCrateDragAndDrop();
// Reset sort dropdowns
document.getElementById('crateBPMSort').value = '';
document.getElementById('crateKeySort').value = '';
}
} else {
const errorMsg = data.error || 'Unknown error occurred';
console.error('API returned error:', errorMsg);
tracksList.innerHTML = `
<div style="text-align: center; padding: 2rem; color: #ef4444;">
<div style="font-size: 2rem; margin-bottom: 0.5rem;">⚠️</div>
<div style="font-weight: 600; margin-bottom: 0.5rem;">${libraryTranslations.crates_error_loading_crate}</div>
<div style="font-size: 0.9rem; color: #a0aec0;">${escapeHtml(errorMsg)}</div>
</div>
`;
showNotification(libraryTranslations.crates_error_loading_crate + ': ' + errorMsg, 'error');
}
})
.catch(error => {
console.error('Error loading crate:', error);
const errorMsg = error.message || 'Failed to load crate. Please check your connection and try again.';
tracksList.innerHTML = `
<div style="text-align: center; padding: 2rem; color: #ef4444;">
<div style="font-size: 2rem; margin-bottom: 0.5rem;">⚠️</div>
<div style="font-weight: 600; margin-bottom: 0.5rem;">${libraryTranslations.crates_error_loading_crate}</div>
<div style="font-size: 0.9rem; color: #a0aec0;">${escapeHtml(errorMsg)}</div>
<div style="margin-top: 1rem; font-size: 0.85rem; color: #a0aec0;">${libraryTranslations.crates_error_loading_desc}</div>
</div>
`;
showNotification(libraryTranslations.crates_error_loading_crate + '. ' + libraryTranslations.crates_error_loading_desc, 'error');
});
}
function closeViewCrateModal() {
const modal = document.getElementById('viewCrateModal');
if (modal) {
modal.style.setProperty('display', 'none', 'important');
}
}
// Crate sorting functions
function sortCrateTracks(sortType, sortOrder) {
if (!window.currentCrateData || !window.currentCrateData.tracks) {
return;
}
const tracks = [...window.currentCrateData.tracks]; // Copy array
const tracksList = document.getElementById('crateTracksList');
// Reset other sort dropdown
if (sortType === 'bpm') {
document.getElementById('crateKeySort').value = '';
} else {
document.getElementById('crateBPMSort').value = '';
}
let sortedTracks = tracks;
if (sortType === 'bpm') {
// Sort by BPM
sortedTracks = tracks.sort((a, b) => {
const aBpm = parseFloat(a.bpm) || 0;
const bBpm = parseFloat(b.bpm) || 0;
if (sortOrder === 'asc') {
return aBpm - bBpm;
} else {
return bBpm - aBpm;
}
});
} else if (sortType === 'key') {
// Camelot wheel order: 1A, 1B, 2A, 2B, ..., 12A, 12B
const camelotOrder = {
'1A': 1, '1B': 2, '2A': 3, '2B': 4, '3A': 5, '3B': 6,
'4A': 7, '4B': 8, '5A': 9, '5B': 10, '6A': 11, '6B': 12,
'7A': 13, '7B': 14, '8A': 15, '8B': 16, '9A': 17, '9B': 18,
'10A': 19, '10B': 20, '11A': 21, '11B': 22, '12A': 23, '12B': 24
};
if (sortOrder === 'harmonic') {
// Harmonic Journey: 1A→12B then 12B→1A (round trip)
// First sort all tracks by key ascending (1A→12B)
sortedTracks = tracks.sort((a, b) => {
const aCamelot = (a.camelot || '').toUpperCase();
const bCamelot = (b.camelot || '').toUpperCase();
const aOrder = camelotOrder[aCamelot] || 999;
const bOrder = camelotOrder[bCamelot] || 999;
return aOrder - bOrder;
});
// Split into two halves: first half ascending, second half descending
const midpoint = Math.ceil(sortedTracks.length / 2);
const firstHalf = sortedTracks.slice(0, midpoint);
const secondHalf = sortedTracks.slice(midpoint).sort((a, b) => {
const aCamelot = (a.camelot || '').toUpperCase();
const bCamelot = (b.camelot || '').toUpperCase();
const aOrder = camelotOrder[aCamelot] || 999;
const bOrder = camelotOrder[bCamelot] || 999;
return bOrder - aOrder; // Reverse order for second half
});
sortedTracks = [...firstHalf, ...secondHalf];
} else {
// Standard key sorting (ascending or descending)
sortedTracks = tracks.sort((a, b) => {
const aCamelot = (a.camelot || '').toUpperCase();
const bCamelot = (b.camelot || '').toUpperCase();
const aOrder = camelotOrder[aCamelot] || 999;
const bOrder = camelotOrder[bCamelot] || 999;
if (sortOrder === 'asc') {
return aOrder - bOrder;
} else {
return bOrder - aOrder;
}
});
}
}
// Update currentCrateData with sorted tracks
window.currentCrateData.tracks = sortedTracks;
// Re-render tracks with new order
const escapeHtml = (str) => {
if (!str) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
};
const escapeHtmlAttr = (str) => {
if (!str) return '';
return String(str)
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '<')
.replace(/>/g, '>');
};
const crateId = window.currentCrateData.id;
tracksList.innerHTML = sortedTracks.map((track, index) => {
const trackDuration = parseFloat(track.duration) || 0;
const setDuration = parseFloat(track.set_duration_minutes) || (trackDuration >= 300 ? 2.5 : trackDuration * 0.5 / 60);
const minutes = Math.floor(trackDuration / 60);
const seconds = Math.floor(trackDuration % 60);
const position = index + 1;
const bpmDisplay = track.bpm ? track.bpm.toFixed(1) : '—';
const keyDisplay = track.key ? track.key : '—';
const camelotDisplay = track.camelot ? track.camelot : '';
return `
<div class="crate-track-item"
data-track-id="${track.id}"
data-position="${position}"
data-bpm="${track.bpm || ''}"
data-camelot="${track.camelot || ''}"
style="display: flex; align-items: center; gap: 1rem; padding: 1rem; background: rgba(255, 255, 255, 0.03); border-radius: 10px; border: 1px solid rgba(255, 255, 255, 0.1); transition: all 0.3s ease; cursor: move;"
onmouseover="this.style.background='rgba(255, 255, 255, 0.06)'"
onmouseout="this.style.background='rgba(255, 255, 255, 0.03)'"
draggable="true"
ondragstart="handleDragStart(event, ${track.id})"
ondragover="handleDragOver(event)"
ondrop="handleDrop(event, ${track.id}, ${crateId})"
ondragend="handleDragEnd(event)">
<div style="display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0;">
<div class="drag-handle" style="color: #a0aec0; cursor: grab; padding: 0.25rem;" title="${libraryTranslations.crates_drag_reorder}">
<i class="fas fa-grip-vertical"></i>
</div>
<div style="width: 2.5rem; height: 2.5rem; border-radius: 8px; background: linear-gradient(135deg, #667eea, #764ba2); display: flex; align-items: center; justify-content: center; color: white; font-weight: 600; font-size: 0.9rem;">
${position}
</div>
</div>
<div style="flex: 1; min-width: 0;">
<div style="font-weight: 600; color: white; margin-bottom: 0.25rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${escapeHtml(track.title || libraryTranslations.untitled_track)}</div>
<div style="font-size: 0.9rem; color: #a0aec0; display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap;">
<span>${minutes}:${String(seconds).padStart(2, '0')}</span>
<span>•</span>
<span><strong>BPM:</strong> ${bpmDisplay}</span>
<span>•</span>
<span><strong>Key:</strong> ${keyDisplay}${camelotDisplay ? ' <span style="color: #667eea; font-weight: 600;">(' + camelotDisplay + ')</span>' : ''}</span>
<span>•</span>
<span>${libraryTranslations.crates_set_duration}: ${setDuration.toFixed(1)} ${libraryTranslations.crates_min_set}</span>
</div>
</div>
<div style="display: flex; gap: 0.5rem; flex-shrink: 0;">
<button onclick="event.stopPropagation(); window.playCrateFromTrack(${index})"
style="background: rgba(16, 185, 129, 0.2); color: #10b981; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.9rem; transition: all 0.3s ease;"
onmouseover="this.style.background='rgba(16, 185, 129, 0.3)'"
onmouseout="this.style.background='rgba(16, 185, 129, 0.2)'"
title="Play from here">
<i class="fas fa-play"></i>
</button>
<button onclick="toggleCrateTrackVisibility(${crateId}, ${track.id}, this)"
style="background: ${track.crate_track_public == 1 || track.crate_track_public === undefined ? 'rgba(102, 126, 234, 0.25)' : 'rgba(107, 114, 128, 0.25)'}; color: ${track.crate_track_public == 1 || track.crate_track_public === undefined ? '#667eea' : '#6b7280'}; border: 1px solid ${track.crate_track_public == 1 || track.crate_track_public === undefined ? 'rgba(102, 126, 234, 0.4)' : 'rgba(107, 114, 128, 0.4)'}; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.9rem; transition: all 0.3s ease;"
data-is-public="${track.crate_track_public == 1 || track.crate_track_public === undefined ? '1' : '0'}"
title="${track.crate_track_public == 1 || track.crate_track_public === undefined ? (libraryTranslations.crates_track_public || 'Track visible in public crate') : (libraryTranslations.crates_track_private || 'Track hidden from public crate')}">
<i class="fas ${track.crate_track_public == 1 || track.crate_track_public === undefined ? 'fa-eye' : 'fa-eye-slash'}"></i>
</button>
${(track.user_owns_track == 1 || track.user_purchased_track == 1) ? `
<button onclick="window.downloadCrateTrack(${track.id}, ${crateId}, ${position})"
style="background: rgba(102, 126, 234, 0.2); color: #667eea; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.9rem; transition: all 0.3s ease;"
onmouseover="this.style.background='rgba(102, 126, 234, 0.3)'"
onmouseout="this.style.background='rgba(102, 126, 234, 0.2)'"
title="${libraryTranslations.crates_download}: ${escapeHtml(window.currentCrateData.name)} - Track ${position}">
<i class="fas fa-download"></i>
</button>
` : `
<button onclick="event.stopPropagation(); window.addToCartFromCrate(${track.id}, this)"
data-track-id="${track.id}"
data-track-title="${escapeHtmlAttr(track.title || 'Untitled Track')}"
data-track-price="${track.price || 0}"
class="btn-cart"
style="background: rgba(245, 158, 11, 0.2); color: #f59e0b; border: none; padding: 0.5rem; border-radius: 6px; cursor: pointer; font-size: 0.85rem; transition: all 0.3s ease; min-width: 36px; display: flex; align-items: center; justify-content: center;"
onmouseover="this.style.background='rgba(245, 158, 11, 0.3)'"
onmouseout="this.style.background='rgba(245, 158, 11, 0.2)'"
title="${libraryTranslations.crates_add_to_cart || 'Add to Cart'}${track.price && track.price > 0 ? ' ($' + track.price + ')' : ' (' + (libraryTranslations.free || 'Free') + ')'}">
<i class="fas fa-shopping-cart"></i>
</button>
`}
<button onclick="removeTrackFromCrate(${crateId}, ${track.id})"
style="background: rgba(239, 68, 68, 0.2); color: #ef4444; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.9rem; transition: all 0.3s ease;"
onmouseover="this.style.background='rgba(239, 68, 68, 0.3)'"
onmouseout="this.style.background='rgba(239, 68, 68, 0.2)'"
title="${libraryTranslations.crates_remove}">
<i class="fas fa-times"></i>
</button>
</div>
</div>
`;
}).join('');
// Re-initialize drag and drop
initCrateDragAndDrop();
}
// Make crate modal draggable
(function initDraggableModal() {
let isDragging = false;
let dragOffsetX = 0;
let dragOffsetY = 0;
const header = document.getElementById('crateModalHeader');
const modal = document.getElementById('crateModalContent');
if (!header || !modal) return;
header.addEventListener('mousedown', function(e) {
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'I' && e.target.closest('button')) return;
isDragging = true;
const rect = modal.getBoundingClientRect();
dragOffsetX = e.clientX - rect.left;
dragOffsetY = e.clientY - rect.top;
// Remove transform for proper positioning
modal.style.transform = 'none';
modal.style.left = rect.left + 'px';
modal.style.top = rect.top + 'px';
document.body.style.userSelect = 'none';
});
document.addEventListener('mousemove', function(e) {
if (!isDragging) return;
let newX = e.clientX - dragOffsetX;
let newY = e.clientY - dragOffsetY;
// Keep within viewport
newX = Math.max(0, Math.min(newX, window.innerWidth - modal.offsetWidth));
newY = Math.max(0, Math.min(newY, window.innerHeight - modal.offsetHeight));
modal.style.left = newX + 'px';
modal.style.top = newY + 'px';
});
document.addEventListener('mouseup', function() {
isDragging = false;
document.body.style.userSelect = '';
});
})();
// Build playlist from crate data - includes BOTH variations for each track
window.buildCratePlaylist = function(crateData) {
const playlistTracks = [];
crateData.tracks.forEach((track, index) => {
const artistName = track.artist_name || 'Unknown Artist';
const position = track.position || (index + 1);
const baseTitle = track.title || 'Untitled';
// Check if track has variations
if (track.variations && track.variations.length > 0) {
// Add each variation as a separate playlist entry
track.variations.forEach((variation, varIndex) => {
if (variation.audio_url) {
const varLabel = variation.title || `Version ${varIndex + 1}`;
playlistTracks.push({
id: `${track.id}_v${variation.variation_index}`,
audio_url: variation.audio_url,
title: `${position}. ${baseTitle} - ${varLabel}`,
artist_name: artistName,
duration: variation.duration || track.duration,
trackId: track.id,
variationIndex: variation.variation_index
});
}
});
} else if (track.audio_url) {
// No variations - just add the main track
playlistTracks.push({
id: track.id,
audio_url: track.audio_url,
title: `${position}. ${baseTitle}`,
artist_name: artistName,
duration: track.duration,
trackId: track.id,
variationIndex: 0
});
}
});
return playlistTracks;
};
// Play crate as playlist with all variations
window.playCrateAsPlaylist = function() {
console.log('🎵 playCrateAsPlaylist called');
console.log('🎵 currentCrateData:', window.currentCrateData);
if (!window.currentCrateData || !window.currentCrateData.tracks || window.currentCrateData.tracks.length === 0) {
console.error('🎵 No crate data available');
if (typeof showNotification === 'function') {
showNotification('No tracks in this crate to play', 'error');
} else {
alert('No tracks in this crate to play');
}
return;
}
const crateData = window.currentCrateData;
const playlistTracks = window.buildCratePlaylist(crateData);
console.log('🎵 Built playlist with', playlistTracks.length, 'tracks:', playlistTracks);
if (playlistTracks.length === 0) {
console.error('🎵 No playable tracks found');
if (typeof showNotification === 'function') {
showNotification('No playable tracks found in this crate', 'error');
}
return;
}
// Load playlist into global player
console.log('🎵 Checking for loadCratePlaylist function:', typeof window.loadCratePlaylist);
if (typeof window.loadCratePlaylist === 'function') {
console.log('🎵 Calling loadCratePlaylist...');
window.loadCratePlaylist(playlistTracks, crateData.name);
} else if (typeof window.enhancedGlobalPlayer !== 'undefined' && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
// Fallback: use enhanced global player
console.log('🎵 Fallback: using enhancedGlobalPlayer');
const firstTrack = playlistTracks[0];
window.enhancedGlobalPlayer.playTrack(firstTrack.audio_url, firstTrack.title, firstTrack.artist_name);
if (typeof showNotification === 'function') {
showNotification(`Playing: ${crateData.name} (${playlistTracks.length} tracks)`, 'success');
}
} else {
console.error('🎵 No player available');
alert('Player not available. Please refresh the page.');
}
// Keep modal open so user can see tracks while playing
};
// Play crate starting from a specific track index
window.playCrateFromTrack = function(startIndex) {
if (!window.currentCrateData || !window.currentCrateData.tracks) {
showNotification('No crate data available', 'error');
return;
}
const crateData = window.currentCrateData;
const playlistTracks = buildCratePlaylist(crateData);
if (playlistTracks.length === 0) {
showNotification('No playable tracks found', 'error');
return;
}
// Calculate start index accounting for variations
let playlistStartIndex = 0;
for (let i = 0; i < crateData.tracks.length && i < startIndex; i++) {
const track = crateData.tracks[i];
if (track.variations && track.variations.length > 0) {
playlistStartIndex += track.variations.length;
} else {
playlistStartIndex += 1;
}
}
playlistStartIndex = Math.min(playlistStartIndex, playlistTracks.length - 1);
// Reorder playlist to start from the selected track
if (typeof window.loadCratePlaylist === 'function') {
const reorderedTracks = [
...playlistTracks.slice(playlistStartIndex),
...playlistTracks.slice(0, playlistStartIndex)
];
window.loadCratePlaylist(reorderedTracks, crateData.name);
}
// Keep modal open so user can see tracks while playing
};
function showAddToCrateModal(trackId) {
currentTrackIdForCrate = trackId;
const cratesSelectList = document.getElementById('cratesSelectList');
const modal = document.getElementById('addToCrateModal');
// Show modal immediately with loading state
modal.style.setProperty('display', 'flex', 'important');
cratesSelectList.innerHTML = '<div style="text-align: center; padding: 2rem; color: #a0aec0;"><i class="fas fa-spinner fa-spin" style="font-size: 1.5rem; margin-bottom: 0.5rem;"></i><div>Loading crates...</div></div>';
// Reload crates to ensure we have the latest list, including which crates already have this track
fetch(`/api/get_user_crates.php?track_id=${trackId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
crates = data.crates;
if (crates.length === 0) {
cratesSelectList.innerHTML = `
<div style="text-align: center; padding: 2rem; color: #a0aec0;">
<div style="font-size: 2rem; margin-bottom: 0.5rem;">📦</div>
<div style="margin-bottom: 1rem;">${libraryTranslations.crates_empty_desc}</div>
</div>
`;
} else {
cratesSelectList.innerHTML = crates.map(crate => {
const hasTrack = crate.has_track == 1 || crate.has_track === true;
const isDisabled = hasTrack;
return `
<button onclick="${isDisabled ? '' : `addTrackToCrate(${crate.id}, ${trackId})`}"
style="width: 100%; padding: 1rem; text-align: left; background: ${hasTrack ? 'rgba(16, 185, 129, 0.1)' : 'rgba(255, 255, 255, 0.05)'}; border: 1px solid ${hasTrack ? 'rgba(16, 185, 129, 0.3)' : 'rgba(255, 255, 255, 0.1)'}; border-radius: 8px; color: white; cursor: ${isDisabled ? 'default' : 'pointer'}; transition: all 0.3s ease; margin-bottom: 0.5rem; opacity: ${isDisabled ? '0.7' : '1'};"
onmouseover="${!isDisabled ? `this.style.background='rgba(102, 126, 234, 0.2)'; this.style.borderColor='rgba(102, 126, 234, 0.5)'` : ''}"
onmouseout="${!isDisabled ? `this.style.background='rgba(255, 255, 255, 0.05)'; this.style.borderColor='rgba(255, 255, 255, 0.1)'` : ''}"
${isDisabled ? 'disabled' : ''}>
<div style="display: flex; align-items: center; gap: 0.75rem;">
<div style="width: 2.5rem; height: 2.5rem; border-radius: 8px; background: ${hasTrack ? 'linear-gradient(135deg, #10b981, #059669)' : 'linear-gradient(135deg, #667eea, #764ba2)'}; display: flex; align-items: center; justify-content: center; color: white; font-weight: 600; font-size: 0.9rem;">
<i class="fas ${hasTrack ? 'fa-check' : 'fa-folder'}"></i>
</div>
<div style="flex: 1;">
<div style="font-weight: 600; margin-bottom: 0.25rem; color: white;">${escapeHtml(crate.name)} ${hasTrack ? `<span style="color: #10b981; font-size: 0.85rem;">${libraryTranslations.crates_already_added}</span>` : ''}</div>
<div style="font-size: 0.85rem; color: #a0aec0;">${crate.track_count || 0} ${libraryTranslations.crates_tracks} • ${(crate.set_duration_minutes || 0).toFixed(1)} ${libraryTranslations.crates_min_set}</div>
</div>
<div style="color: ${hasTrack ? '#10b981' : '#667eea'};">
<i class="fas ${hasTrack ? 'fa-check-circle' : 'fa-plus'}"></i>
</div>
</div>
</button>
`;
}).join('');
}
} else {
cratesSelectList.innerHTML = `<div style="text-align: center; padding: 2rem; color: #ef4444;">${libraryTranslations.crates_error_loading}</div>`;
}
})
.catch(error => {
console.error('Error loading crates:', error);
cratesSelectList.innerHTML = '<div style="text-align: center; padding: 2rem; color: #ef4444;">Error loading crates. Please refresh the page.</div>';
});
}
function closeAddToCrateModal() {
const modal = document.getElementById('addToCrateModal');
if (modal) {
modal.style.setProperty('display', 'none', 'important');
}
currentTrackIdForCrate = null;
}
function addTrackToCrate(crateId, trackId) {
// Find crate name for feedback
const crate = crates.find(c => c.id === crateId);
const crateName = crate ? crate.name : 'crate';
// Show loading state on the button
const buttons = document.querySelectorAll(`button[onclick*="addTrackToCrate(${crateId}, ${trackId})"]`);
buttons.forEach(btn => {
const originalHTML = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Adding...';
btn.disabled = true;
btn.style.opacity = '0.6';
btn.style.cursor = 'not-allowed';
});
fetch('/api/add_track_to_crate.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
crate_id: crateId,
track_id: trackId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Restore button state
buttons.forEach(btn => {
btn.innerHTML = '<i class="fas fa-check"></i> Added!';
btn.style.background = 'rgba(16, 185, 129, 0.2)';
btn.style.borderColor = '#10b981';
btn.style.color = '#10b981';
});
// Close modal after short delay and show success
setTimeout(() => {
closeAddToCrateModal();
loadCrates();
showNotification(`${libraryTranslations.crates_track_added.replace('!', '')} "${crateName}"!`, 'success');
}, 500);
} else {
// Restore button state
buttons.forEach(btn => {
btn.innerHTML = '<i class="fas fa-times"></i> Error';
btn.style.background = 'rgba(239, 68, 68, 0.2)';
btn.style.borderColor = '#ef4444';
btn.disabled = false;
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
});
showNotification('Error: ' + (data.error || 'Failed to add track'), 'error');
}
})
.catch(error => {
console.error('Error adding track to crate:', error);
// Restore button state
buttons.forEach(btn => {
btn.innerHTML = '<i class="fas fa-times"></i> Error';
btn.style.background = 'rgba(239, 68, 68, 0.2)';
btn.style.borderColor = '#ef4444';
btn.disabled = false;
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
});
showNotification('Error adding track to crate. Please try again.', 'error');
});
}
// Save crate description
function saveCrateDescription() {
const crateId = window.currentCrateData?.id;
if (!crateId) return;
const descInput = document.getElementById('crateDescriptionInput');
const saveBtn = document.getElementById('saveDescriptionBtn');
const descStatus = document.getElementById('descriptionSaveStatus');
const description = descInput.value.trim();
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
saveBtn.disabled = true;
descStatus.style.display = 'none';
fetch('/api/update_crate_description.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ crate_id: crateId, description: description })
})
.then(response => response.json())
.then(data => {
saveBtn.innerHTML = '<i class="fas fa-save"></i>';
saveBtn.disabled = false;
if (data.success) {
descStatus.textContent = libraryTranslations.crates_desc_saved || 'Description saved!';
descStatus.style.color = '#10b981';
descStatus.style.display = 'block';
setTimeout(() => { descStatus.style.display = 'none'; }, 2000);
// Update crate data in memory
if (window.currentCrateData) {
window.currentCrateData.description = description;
}
} else {
descStatus.textContent = data.error || 'Failed to save';
descStatus.style.color = '#ef4444';
descStatus.style.display = 'block';
}
})
.catch(error => {
saveBtn.innerHTML = '<i class="fas fa-save"></i>';
saveBtn.disabled = false;
descStatus.textContent = 'Error saving description';
descStatus.style.color = '#ef4444';
descStatus.style.display = 'block';
console.error('Error:', error);
});
}
// Update description visibility toggle button
function updateDescriptionToggleButton(isPublic, crateIsPublic = null) {
const btn = document.getElementById('toggleDescriptionVisibilityBtn');
if (!btn) return;
// Use stored crate public status if not provided
if (crateIsPublic === null) {
crateIsPublic = window.currentCrateIsPublic !== false;
}
// If crate is private, disable the toggle (description visibility only matters for public crates)
if (!crateIsPublic) {
btn.disabled = true;
btn.style.opacity = '0.5';
btn.style.cursor = 'not-allowed';
btn.innerHTML = '<i class="fas fa-eye-slash"></i>';
btn.style.color = '#6b7280';
btn.style.borderColor = 'rgba(107, 114, 128, 0.2)';
btn.title = libraryTranslations.crates_desc_private_crate || 'Description visibility only applies to public crates';
return;
}
// Crate is public - enable the toggle
btn.disabled = false;
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
if (isPublic) {
btn.innerHTML = '<i class="fas fa-eye"></i>';
btn.style.color = '#667eea';
btn.style.borderColor = 'rgba(102, 126, 234, 0.4)';
btn.title = libraryTranslations.crates_desc_public || 'Description visible to public';
} else {
btn.innerHTML = '<i class="fas fa-eye-slash"></i>';
btn.style.color = '#6b7280';
btn.style.borderColor = 'rgba(107, 114, 128, 0.4)';
btn.title = libraryTranslations.crates_desc_private || 'Description hidden from public';
}
}
// Toggle crate description visibility
function toggleCrateDescriptionVisibility() {
const crateId = window.currentCrateData?.id;
if (!crateId) {
showCrateNotification('No crate selected', 'error');
return;
}
// Check if crate is public - description visibility only applies to public crates
const crateIsPublic = window.currentCrateIsPublic !== false && (window.currentCrateData?.is_public == 1 || window.currentCrateData?.is_public === true);
if (!crateIsPublic) {
showCrateNotification('Description visibility only applies to public crates. Make the crate public first.', 'info');
return;
}
const btn = document.getElementById('toggleDescriptionVisibilityBtn');
if (!btn || btn.disabled) return;
const originalHTML = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
btn.disabled = true;
fetch('/api/toggle_crate_description_visibility.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ crate_id: crateId })
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text().then(text => {
try {
return JSON.parse(text);
} catch (e) {
console.error('Invalid JSON response:', text);
throw new Error('Invalid response from server');
}
});
})
.then(data => {
btn.disabled = false;
if (data && data.success) {
// Ensure boolean value
const isPublic = (data.is_description_public == 1 || data.is_description_public === true);
window.currentCrateDescriptionPublic = isPublic;
// Update crate data if it exists
if (window.currentCrateData) {
window.currentCrateData.is_description_public = isPublic;
}
updateDescriptionToggleButton(isPublic, window.currentCrateIsPublic);
showCrateNotification(data.message || 'Visibility updated successfully', 'success');
} else {
btn.innerHTML = originalHTML;
const errorMsg = data?.error || 'Failed to update description visibility';
console.error('API error:', data);
showCrateNotification(errorMsg, 'error');
}
})
.catch(error => {
btn.disabled = false;
btn.innerHTML = originalHTML;
console.error('Error toggling description visibility:', error);
showCrateNotification('Failed to update description visibility. Please try again.', 'error');
});
}
// Toggle track visibility within a crate (public/private)
function toggleCrateTrackVisibility(crateId, trackId, button) {
const icon = button.querySelector('i');
const originalIcon = icon.className;
icon.className = 'fas fa-spinner fa-spin';
button.disabled = true;
fetch('/api/toggle_crate_track_visibility.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ crate_id: crateId, track_id: trackId })
})
.then(response => response.json())
.then(data => {
button.disabled = false;
if (data.success) {
const isPublic = data.is_public;
button.setAttribute('data-is-public', isPublic ? '1' : '0');
button.style.background = isPublic ? 'rgba(102, 126, 234, 0.25)' : 'rgba(107, 114, 128, 0.25)';
button.style.color = isPublic ? '#667eea' : '#6b7280';
button.style.borderColor = isPublic ? 'rgba(102, 126, 234, 0.4)' : 'rgba(107, 114, 128, 0.4)';
button.title = isPublic
? (libraryTranslations.crates_track_public || 'Track visible in public crate')
: (libraryTranslations.crates_track_private || 'Track hidden from public crate');
icon.className = isPublic ? 'fas fa-eye' : 'fas fa-eye-slash';
showCrateNotification(data.message, 'success');
} else {
icon.className = originalIcon;
showCrateNotification(data.error || 'Failed to update track visibility', 'error');
}
})
.catch(error => {
button.disabled = false;
icon.className = originalIcon;
console.error('Error toggling track visibility:', error);
showCrateNotification('Error updating visibility. Please try again.', 'error');
});
}
function removeTrackFromCrate(crateId, trackId) {
if (!confirm(libraryTranslations.crates_remove_confirm)) {
return;
}
fetch('/api/remove_track_from_crate.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
crate_id: crateId,
track_id: trackId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
viewCrate(crateId); // Reload crate view
loadCrates(); // Reload crates list
showNotification(libraryTranslations.crates_track_removed, 'success');
} else {
alert('Error removing track: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error removing track from crate:', error);
alert('Error removing track from crate. Please try again.');
});
}
function deleteCrate(crateId) {
if (!confirm(libraryTranslations.crates_delete_confirm)) {
return;
}
fetch('/api/delete_crate.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
crate_id: crateId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
loadCrates();
showNotification(libraryTranslations.crates_crate_deleted, 'success');
} else {
alert('Error deleting crate: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error deleting crate:', error);
alert('Error deleting crate. Please try again.');
});
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showNotification(message, type) {
// Remove any existing notifications first
const existing = document.querySelectorAll('.crate-notification');
existing.forEach(n => n.remove());
const notification = document.createElement('div');
notification.className = 'crate-notification';
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 1rem 1.5rem;
background: ${type === 'success' ? '#10b981' : '#ef4444'};
color: white;
border-radius: 12px;
z-index: 10001;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 600;
font-size: 1rem;
min-width: 250px;
animation: slideInRight 0.3s ease;
backdrop-filter: blur(10px);
`;
const icon = type === 'success' ? '<i class="fas fa-check-circle"></i>' : '<i class="fas fa-exclamation-circle"></i>';
notification.innerHTML = `${icon} <span>${escapeHtml(message)}</span>`;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'slideOutRight 0.3s ease';
setTimeout(() => notification.remove(), 300);
}, 4000);
}
// Add CSS animations for notifications
if (!document.getElementById('crate-notification-styles')) {
const style = document.createElement('style');
style.id = 'crate-notification-styles';
style.textContent = `
@keyframes slideInRight {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
`;
document.head.appendChild(style);
}
// Download crate track
function downloadCrateTrack(trackId, crateId, position) {
console.log('📥 Download crate track called:', { trackId, crateId, position });
if (!trackId) {
console.error('❌ Track ID is required');
if (typeof showNotification === 'function') {
showNotification(libraryTranslations.crates_error_loading || 'Error: Track ID is required', 'error');
} else {
alert('Error: Track ID is required');
}
return;
}
if (!crateId) {
console.error('❌ Crate ID is required');
if (typeof showNotification === 'function') {
showNotification(libraryTranslations.crates_error_loading || 'Error: Crate ID is required', 'error');
} else {
alert('Error: Crate ID is required');
}
return;
}
const url = `/api/download_crate_track.php?track_id=${trackId}&crate_id=${crateId}`;
console.log('📥 Download URL:', url);
try {
// Use window.open for better download handling
const downloadWindow = window.open(url, '_blank');
// If popup was blocked, try using a link element
if (!downloadWindow || downloadWindow.closed || typeof downloadWindow.closed === 'undefined') {
console.log('📥 Popup blocked, using link element instead');
const link = document.createElement('a');
link.href = url;
link.download = ''; // Let the server set the filename
link.style.display = 'none';
document.body.appendChild(link);
link.click();
setTimeout(() => {
document.body.removeChild(link);
}, 100);
}
console.log('📥 Download initiated');
} catch (error) {
console.error('❌ Download error:', error);
if (typeof showNotification === 'function') {
showNotification('Error initiating download. Please try again.', 'error');
} else {
alert('Error initiating download. Please try again.');
}
}
}
// Make function globally accessible
window.downloadCrateTrack = downloadCrateTrack;
// Download crate as ZIP
function downloadCrateAsZip() {
console.log('📦 Download crate as ZIP called');
if (!window.currentCrateData || !window.currentCrateData.id) {
console.error('❌ No crate data available');
if (typeof showNotification === 'function') {
showNotification('No crate selected. Please open a crate first.', 'error');
} else {
alert('No crate selected. Please open a crate first.');
}
return;
}
const crateId = window.currentCrateData.id;
const url = `/api/download_crate_zip.php?crate_id=${crateId}`;
console.log('📦 Download ZIP URL:', url);
// Show loading state
const downloadBtn = document.getElementById('downloadCrateBtn');
if (downloadBtn) {
const originalHTML = downloadBtn.innerHTML;
downloadBtn.disabled = true;
downloadBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Preparing...';
// Reset button after 5 seconds (download should have started)
setTimeout(() => {
downloadBtn.disabled = false;
downloadBtn.innerHTML = originalHTML;
}, 5000);
}
try {
// Use window.open for better download handling
const downloadWindow = window.open(url, '_blank');
// If popup was blocked, try using a link element
if (!downloadWindow || downloadWindow.closed || typeof downloadWindow.closed === 'undefined') {
console.log('📦 Popup blocked, using link element instead');
const link = document.createElement('a');
link.href = url;
link.download = ''; // Let the server set the filename
link.style.display = 'none';
document.body.appendChild(link);
link.click();
setTimeout(() => {
document.body.removeChild(link);
}, 100);
}
console.log('📦 ZIP download initiated');
if (typeof showNotification === 'function') {
showNotification('Preparing ZIP download... This may take a moment.', 'info');
}
} catch (error) {
console.error('❌ ZIP download error:', error);
if (downloadBtn) {
downloadBtn.disabled = false;
downloadBtn.innerHTML = '<i class="fas fa-download"></i> ' + (libraryTranslations.crates_download_all || 'Download Crate');
}
if (typeof showNotification === 'function') {
showNotification('Error initiating ZIP download. Please try again.', 'error');
} else {
alert('Error initiating ZIP download. Please try again.');
}
}
}
window.downloadCrateAsZip = downloadCrateAsZip;
// Add to cart from crate (simplified version for crate tracks)
// Define and assign to window immediately for global access
window.addToCartFromCrate = function(trackId, buttonElement) {
// Get title and price from data attributes
const button = buttonElement || event.target.closest('.btn-cart');
const title = button ? (button.getAttribute('data-track-title') || 'Untitled Track') : 'Untitled Track';
const price = button ? parseFloat(button.getAttribute('data-track-price') || 0) : 0;
console.log('🛒 Adding to cart from crate:', { trackId, title, price });
if (!<?= $user_id ? 'true' : 'false' ?>) {
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;
}
const originalHTML = button ? button.innerHTML : '';
// Show loading state
if (button) {
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
}
// Send to cart (default to 'free' artist plan)
const formData = new FormData();
formData.append('track_id', trackId);
formData.append('action', 'add');
formData.append('artist_plan', 'free');
fetch('/cart.php', {
method: 'POST',
body: formData
})
.then(response => {
console.log('🛒 Response status:', response.status);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('🛒 Cart response:', data);
if (!data.success) {
throw new Error(data.message || 'Failed to add to cart');
}
// Show success notification
if (price == 0 || !price) {
if (typeof showNotification === 'function') {
const message = (libraryTranslations.added_to_cart_free || `🎵 "${title}" added to cart for FREE! 🛒`).replace(':title', title);
showNotification(message, 'success');
}
} else {
if (typeof showNotification === 'function') {
const message = (libraryTranslations.added_to_cart || `"${title}" added to cart! ($${price})`).replace(':title', title).replace(':price', price);
showNotification(message, 'success');
}
}
// Update cart counter in header - use querySelectorAll to update all instances
const cartCounts = document.querySelectorAll('.cart-count, .cart-counter');
if (cartCounts.length > 0 && data.cart_count !== undefined) {
cartCounts.forEach(count => {
count.textContent = data.cart_count;
// Show the badge if count > 0
if (data.cart_count > 0) {
count.style.display = 'flex';
if (count.classList.contains('cart-counter')) {
count.style.display = 'block';
}
} else {
count.style.display = 'none';
}
});
} else if (cartCounts.length > 0) {
// Fallback: manually increment if API didn't return count
cartCounts.forEach(count => {
const currentCount = parseInt(count.textContent) || 0;
count.textContent = currentCount + 1;
count.style.display = 'flex';
if (count.classList.contains('cart-counter')) {
count.style.display = 'block';
}
});
}
// Show success state on button
if (button) {
button.innerHTML = '<i class="fas fa-check"></i>';
button.style.background = 'rgba(16, 185, 129, 0.3)';
button.style.color = '#10b981';
setTimeout(() => {
button.innerHTML = originalHTML;
button.style.background = '';
button.style.color = '';
button.disabled = false;
}, 2000);
}
})
.catch(error => {
console.error('🛒 Cart error:', error);
if (typeof showNotification === 'function') {
showNotification('Failed to add to cart: ' + error.message, 'error');
} else {
alert('Failed to add to cart: ' + error.message);
}
// Restore button
if (button) {
button.innerHTML = originalHTML;
button.disabled = false;
}
});
};
// Drag and Drop for reordering tracks
let draggedTrackId = null;
let draggedElement = null;
function handleDragStart(e, trackId) {
draggedTrackId = trackId;
draggedElement = e.currentTarget;
e.currentTarget.style.opacity = '0.5';
e.dataTransfer.effectAllowed = 'move';
}
function handleDragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
e.dataTransfer.dropEffect = 'move';
const trackItem = e.currentTarget.closest('.crate-track-item');
if (trackItem && trackItem !== draggedElement) {
trackItem.style.borderTop = '2px solid #667eea';
}
return false;
}
function handleDrop(e, targetTrackId, crateId) {
if (e.stopPropagation) {
e.stopPropagation();
}
if (draggedTrackId === targetTrackId) {
return false;
}
const trackItem = e.currentTarget.closest('.crate-track-item');
if (trackItem) {
trackItem.style.borderTop = '';
}
// Get current order
const trackItems = Array.from(document.querySelectorAll('.crate-track-item'));
const currentOrder = trackItems.map(item => parseInt(item.dataset.trackId));
// Find indices
const draggedIndex = currentOrder.indexOf(draggedTrackId);
const targetIndex = currentOrder.indexOf(targetTrackId);
// Reorder array
const newOrder = [...currentOrder];
newOrder.splice(draggedIndex, 1);
newOrder.splice(targetIndex, 0, draggedTrackId);
// Update positions in UI
updateTrackOrder(crateId, newOrder);
return false;
}
function handleDragEnd(e) {
e.currentTarget.style.opacity = '1';
const trackItems = document.querySelectorAll('.crate-track-item');
trackItems.forEach(item => {
item.style.borderTop = '';
});
draggedTrackId = null;
draggedElement = null;
}
function updateTrackOrder(crateId, trackOrder) {
// Show loading state
const tracksList = document.getElementById('crateTracksList');
const originalHTML = tracksList.innerHTML;
tracksList.innerHTML = `<div style="text-align: center; padding: 2rem; color: #a0aec0;"><i class="fas fa-spinner fa-spin" style="font-size: 1.5rem;"></i><div>${libraryTranslations.crates_updating_order}</div></div>`;
fetch('/api/update_crate_track_order.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
crate_id: crateId,
track_order: trackOrder
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Reload crate view
viewCrate(crateId);
showNotification(libraryTranslations.crates_order_updated, 'success');
} else {
tracksList.innerHTML = originalHTML;
showNotification('Error updating order: ' + (data.error || 'Unknown error'), 'error');
}
})
.catch(error => {
console.error('Error updating track order:', error);
tracksList.innerHTML = originalHTML;
showNotification('Error updating track order. Please try again.', 'error');
});
}
function initCrateDragAndDrop() {
// Add visual feedback for drag over
const trackItems = document.querySelectorAll('.crate-track-item');
trackItems.forEach(item => {
item.addEventListener('dragover', function(e) {
if (e.preventDefault) {
e.preventDefault();
}
if (this !== draggedElement) {
this.style.borderTop = '2px solid #667eea';
}
});
item.addEventListener('dragleave', function(e) {
this.style.borderTop = '';
});
});
}
// Close modals when clicking outside
document.addEventListener('click', function(e) {
if (e.target.classList.contains('modal')) {
e.target.style.display = 'none';
}
});
// Track Image Download Handler
function handleTrackImageDownload(e) {
e.preventDefault();
e.stopPropagation();
const imageUrl = e.currentTarget.dataset.imageUrl;
const trackTitle = e.currentTarget.dataset.trackTitle || 'track-image';
if (!imageUrl) return;
// Create a temporary anchor element to trigger download
const link = document.createElement('a');
link.href = imageUrl;
// Extract filename from URL or create one from track title
let filename = imageUrl.split('/').pop();
if (!filename || filename === imageUrl) {
// If no filename in URL, create one from track title
const sanitizedTitle = trackTitle.replace(/[^a-z0-9]/gi, '_').toLowerCase();
const extension = imageUrl.match(/\.(jpg|jpeg|png|gif|webp)$/i);
filename = sanitizedTitle + (extension ? extension[0] : '.jpg');
}
link.download = filename;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// Track Image Upload Handler
function handleTrackImageUpload(e) {
const file = e.target.files[0];
if (!file) return;
const trackId = e.target.dataset.trackId;
const container = e.target.closest('.track-image-container, .track-icon');
const overlay = container ? container.querySelector('.track-image-upload-overlay') : null;
if (!trackId || !container) return;
// Validate file type
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
if (typeof showNotification === 'function') {
showNotification(libraryTranslations.image_invalid_file_type, 'error');
} else {
alert(libraryTranslations.image_invalid_file_type);
}
e.target.value = '';
return;
}
// Validate file size (5MB max)
if (file.size > 5 * 1024 * 1024) {
if (typeof showNotification === 'function') {
showNotification(libraryTranslations.image_file_too_large, 'error');
} else {
alert(libraryTranslations.image_file_too_large);
}
e.target.value = '';
return;
}
// Show uploading state
if (overlay) {
overlay.classList.add('uploading');
const uploadIcon = overlay.querySelector('.track-image-upload-icon');
if (uploadIcon) {
uploadIcon.className = 'fas fa-spinner track-image-upload-icon';
}
}
// Create FormData
const formData = new FormData();
formData.append('cover_image', file);
formData.append('track_id', trackId);
// Upload image
fetch('/api/upload_track_cover.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success && data.data && data.data.image_url) {
const newImageUrl = data.data.image_url;
// Update the image source
const img = container.querySelector('.track-image');
if (img) {
img.src = newImageUrl + '?t=' + Date.now(); // Add timestamp to prevent caching
// Update data attribute
container.setAttribute('data-image-url', newImageUrl);
// Update download button if it exists
const downloadBtn = container.querySelector('.track-image-download-btn');
if (downloadBtn) {
downloadBtn.setAttribute('data-image-url', newImageUrl);
}
} else {
// If no image exists, convert icon to image container
const iconContainer = container;
if (iconContainer.classList.contains('track-icon')) {
const trackTitle = iconContainer.closest('.track-card-modern')?.querySelector('.track-name')?.textContent?.trim() || 'track-image';
iconContainer.classList.remove('track-icon');
iconContainer.classList.add('track-image-container');
iconContainer.setAttribute('data-image-url', newImageUrl);
iconContainer.innerHTML = `
<img src="${newImageUrl}?t=${Date.now()}" alt="Track cover" class="track-image" loading="lazy">
<div class="track-image-upload-overlay">
<button type="button" class="track-image-action-button track-image-download-btn" data-image-url="${newImageUrl}" data-track-title="${trackTitle.replace(/"/g, '"')}" title="${libraryTranslations.image_download}">
<i class="fas fa-download track-image-download-icon"></i>
</button>
<label class="track-image-action-button" for="track-image-upload-dynamic-${trackId}" title="${libraryTranslations.image_upload || 'Upload image'}">
<i class="fas fa-camera track-image-upload-icon"></i>
<input type="file" id="track-image-upload-dynamic-${trackId}" class="track-image-upload-input" accept="image/jpeg,image/jpg,image/png,image/gif,image/webp" data-track-id="${trackId}">
</label>
</div>
`;
// Re-attach event listeners
const newInput = iconContainer.querySelector('.track-image-upload-input');
if (newInput) {
newInput.addEventListener('change', handleTrackImageUpload);
}
const newDownloadBtn = iconContainer.querySelector('.track-image-download-btn');
if (newDownloadBtn) {
newDownloadBtn.addEventListener('click', handleTrackImageDownload);
}
}
}
if (typeof showNotification === 'function') {
showNotification('✅ ' + libraryTranslations.image_upload_success, 'success');
}
} else {
throw new Error(data.error || libraryTranslations.image_upload_failed);
}
})
.catch(error => {
console.error('Error uploading track image:', error);
if (typeof showNotification === 'function') {
const errorMsg = error.message ? libraryTranslations.image_upload_failed + ': ' + error.message : libraryTranslations.image_upload_failed_unknown;
showNotification('❌ ' + errorMsg, 'error');
} else {
alert(libraryTranslations.image_upload_failed_retry);
}
})
.finally(() => {
// Reset uploading state
if (overlay) {
overlay.classList.remove('uploading');
const uploadIcon = overlay.querySelector('.track-image-upload-icon');
if (uploadIcon) {
uploadIcon.className = 'fas fa-camera track-image-upload-icon';
}
}
// Reset input
e.target.value = '';
});
}
// Open stem separation modal for a specific track
function openStemSeparationForTrack(trackId) {
if (typeof openAdvancedFunctionsModal === 'function') {
openAdvancedFunctionsModal('vocalRemoval', trackId);
} else {
// Fallback: redirect to studio
if (window.ajaxNavigation) {
window.ajaxNavigation.navigateToPage('/studio.php');
} else {
window.location.href = '/studio.php';
}
}
}
// Show stems modal for a track that has stems
function showStemsModal(trackId) {
// Fetch track data to get stems
fetch(`/api/get_track_details.php?track_id=${trackId}`)
.then(response => response.json())
.then(data => {
if (data.success && data.track) {
const track = data.track;
const metadata = track.metadata || {};
// Check both stem_files and stems arrays
let stemFiles = metadata.stem_files || [];
if (stemFiles.length === 0 && metadata.stems) {
// Convert stems format to stem_files format (legacy support)
stemFiles = metadata.stems.map(stem => ({
name: stem.title || stem.type || stem.stem_name || 'Stem',
url: stem.audio_url || stem.url || '',
type: stem.type || 'unknown',
index: stem.index || 0
}));
}
// Normalize stem data structure
stemFiles = stemFiles.map((stem, idx) => ({
name: stem.name || stem.title || stem.stem_name || `Stem ${idx + 1}`,
url: stem.url || stem.audio_url || stem.original_url || '',
type: stem.type || 'unknown',
index: stem.index !== undefined ? stem.index : idx
})).filter(stem => stem.url); // Filter out stems without URLs
if (stemFiles.length === 0) {
alert('No stems available for this track.');
return;
}
// Populate stems grid
const stemsGrid = document.getElementById('stemsGrid');
if (!stemsGrid) {
console.error('Stems grid not found');
return;
}
stemsGrid.innerHTML = '';
stemFiles.forEach((stem, index) => {
const stemName = stem.name;
const stemUrl = stem.url;
const stemType = stem.type;
// Escape for HTML/JavaScript
const safeStemName = stemName.replace(/'/g, "\\'").replace(/"/g, '"');
const safeStemUrl = stemUrl.replace(/'/g, "\\'").replace(/"/g, '"');
const safeFileName = stemName.replace(/[^a-z0-9]/gi, '_') + '.mp3';
const stemCard = document.createElement('div');
stemCard.className = 'variation-card';
stemCard.innerHTML = `
<div class="variation-info">
<h3>${stemName}</h3>
<p style="color: #a0aec0; font-size: 0.9rem; margin-top: 0.5rem;">${stemType}</p>
<div class="variation-actions">
<button class="btn-play-variation" onclick="playStem('${safeStemUrl}', '${safeStemName}')">
<i class="fas fa-play"></i> Play
</button>
<a href="${safeStemUrl}" download="${safeFileName}" class="btn-download-variation">
<i class="fas fa-download"></i> Download
</a>
</div>
</div>
`;
stemsGrid.appendChild(stemCard);
});
// Show modal
const modal = document.getElementById('stemsModal');
if (modal) {
modal.classList.add('active');
}
} else {
alert('Failed to load track stems.');
}
})
.catch(error => {
console.error('Error loading stems:', error);
alert('Error loading stems. Please try again.');
});
}
// Close stems modal
function closeStemsModal() {
const modal = document.getElementById('stemsModal');
if (modal) {
modal.classList.remove('active');
}
}
// Play a stem
function playStem(stemUrl, stemName) {
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
window.enhancedGlobalPlayer.playTrack(stemUrl, stemName, 'SoundStudioPro');
} else {
// Fallback: create audio element
const audio = new Audio(stemUrl);
audio.play().catch(err => {
console.error('Error playing stem:', err);
alert('Error playing stem. Please try downloading it instead.');
});
}
}
// Analyze Single Track from Library
let isAnalyzing = false;
let currentAnalyzingTrackId = null;
async function analyzeSingleTrack(trackId, trackTitle) {
if (isAnalyzing) {
showNotification('Analysis already in progress. Please wait.', 'warning');
return;
}
isAnalyzing = true;
currentAnalyzingTrackId = trackId;
// Create analysis overlay
const overlay = createAnalysisOverlay();
setTimeout(() => overlay.classList.add('active'), 10);
updateAnalysisStatus('loading', 10);
try {
// Get signed audio URL with proper authentication
updateAnalysisStatus('loading', 15);
const tokenResponse = await fetch(`/api/get_audio_token.php?track_id=${trackId}`);
if (!tokenResponse.ok) {
throw new Error(`Failed to get audio token: HTTP ${tokenResponse.status}`);
}
const tokenData = await tokenResponse.json();
if (!tokenData.success || !tokenData.url) {
throw new Error(tokenData.error || 'Failed to get valid audio URL');
}
let audioUrl = tokenData.url;
// Make URL absolute if it's relative
if (!audioUrl.startsWith('http')) {
audioUrl = window.location.origin + audioUrl;
}
if (!audioUrl || audioUrl.includes('error')) {
throw new Error('Invalid audio URL received');
}
// Load simple analyzer script if needed
if (!window.simpleAudioAnalyzer) {
updateAnalysisStatus('loading', 20);
await loadSimpleAnalyzerScript();
}
if (!window.simpleAudioAnalyzer) {
throw new Error('Audio analyzer not loaded. Please refresh the page.');
}
updateAnalysisStatus('decoding', 30);
// Analyze audio with simple, lightweight analyzer
const result = await window.simpleAudioAnalyzer.analyzeAudio(audioUrl, (stage, progress) => {
if (stage === 'loading') {
updateAnalysisStatus('loading', progress);
} else if (stage === 'decoding') {
updateAnalysisStatus('decoding', progress);
} else if (stage === 'detecting_bpm' || stage === 'bpm') {
updateAnalysisStatus('bpm', progress);
} else if (stage === 'detecting_key' || stage === 'key') {
updateAnalysisStatus('key', progress);
} else if (stage === 'complete') {
updateAnalysisStatus('complete', 100);
}
});
if (!result || !result.bpm || !result.key) {
throw new Error('Analysis returned incomplete results');
}
// Show completion
updateAnalysisStatus('complete', 100);
await new Promise(resolve => setTimeout(resolve, 500));
overlay.classList.remove('active');
setTimeout(() => overlay.remove(), 300);
// Update UI
const roundedBPM = Math.round(result.bpm * 10) / 10;
updateTrackCardBPM(trackId, roundedBPM);
updateTrackCardKey(trackId, result.key, result.camelot || '');
// Save to server
const saveResponse = await fetch('/api/save_audio_analysis.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
track_id: trackId,
bpm: roundedBPM,
key: result.key,
camelot: result.camelot || '',
energy: result.energy || 'Medium',
confidence: result.confidence || 50
})
});
const saveData = await saveResponse.json();
if (saveData.success) {
showNotification(`Analysis complete for "${trackTitle}"`, 'success');
// Reload page to show updated values
setTimeout(() => window.location.reload(), 1000);
} else {
throw new Error(saveData.error || 'Failed to save analysis');
}
} catch (error) {
console.error('Analysis error:', error);
overlay.classList.remove('active');
setTimeout(() => overlay.remove(), 300);
showNotification('Analysis failed: ' + error.message, 'error');
} finally {
isAnalyzing = false;
currentAnalyzingTrackId = null;
}
}
// Helper functions
function createAnalysisOverlay() {
const overlay = document.createElement('div');
overlay.className = 'analysis-overlay';
overlay.innerHTML = `
<div class="analysis-modal">
<div class="analysis-modal-header">
<h3>Analyzing Audio</h3>
<p>Detecting BPM and musical key...</p>
</div>
<div class="analysis-spinner"></div>
<div class="analysis-progress">
<div class="analysis-progress-bar" id="analysisProgressBar"></div>
</div>
<div class="analysis-status" id="analysisStatus">
<div class="analysis-status-item">
<span class="analysis-status-label">Loading audio...</span>
<span class="analysis-status-value">
<i class="fas fa-spinner fa-spin"></i>
</span>
</div>
</div>
</div>
`;
document.body.appendChild(overlay);
return overlay;
}
function updateAnalysisStatus(stage, progress) {
const statusEl = document.getElementById('analysisStatus');
const progressBar = document.getElementById('analysisProgressBar');
if (progressBar) {
progressBar.style.width = progress + '%';
}
if (statusEl) {
const stages = {
'loading': { label: 'Loading audio file...', icon: 'fa-spinner' },
'decoding': { label: 'Decoding audio data...', icon: 'fa-spinner' },
'bpm': { label: 'Detecting BPM...', icon: 'fa-spinner' },
'key': { label: 'Detecting musical key...', icon: 'fa-spinner' },
'complete': { label: 'Analysis complete!', icon: 'fa-check' }
};
const currentStage = stages[stage] || { label: stage, icon: 'fa-spinner' };
statusEl.innerHTML = `
<div class="analysis-status-item">
<span class="analysis-status-label">${currentStage.label}</span>
<span class="analysis-status-value">
<i class="fas ${currentStage.icon} ${stage !== 'complete' ? 'fa-spin' : ''}"></i>
</span>
</div>
`;
}
}
async function loadSimpleAnalyzerScript() {
if (window.simpleAnalyzerScriptLoaded) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = '/js/audio_analyzer_simple.js';
script.async = true;
script.onload = () => {
window.simpleAnalyzerScriptLoaded = true;
setTimeout(() => {
try {
const analyzer = window.simpleAudioAnalyzer;
if (analyzer) {
resolve();
} else {
reject(new Error('Analyzer failed to initialize'));
}
} catch (e) {
reject(new Error('Analyzer initialization error: ' + e.message));
}
}, 100);
};
script.onerror = () => reject(new Error('Failed to load analyzer script'));
document.head.appendChild(script);
});
}
function updateTrackCardBPM(trackId, bpm) {
const bpmMeta = document.getElementById(`bpm-meta-${trackId}`);
if (bpmMeta) {
const bpmDisplay = bpmMeta.querySelector('.bpm-value-display');
if (bpmDisplay) {
bpmDisplay.textContent = bpm;
}
// Add edit button if it doesn't exist
if (!bpmMeta.querySelector('.bpm-edit-btn')) {
const editBtn = document.createElement('button');
editBtn.className = 'bpm-edit-btn';
editBtn.onclick = () => openBPMCorrectionModal(trackId, bpm);
editBtn.title = 'Edit BPM';
editBtn.style.cssText = 'background: none; border: none; color: #667eea; cursor: pointer; margin-left: 4px; padding: 2px 4px;';
editBtn.innerHTML = '<i class="fas fa-edit" style="font-size: 0.75rem;"></i>';
bpmMeta.appendChild(editBtn);
}
}
}
function updateTrackCardKey(trackId, key, camelot) {
const keyMeta = document.getElementById(`key-meta-${trackId}`);
if (keyMeta) {
const keyDisplay = keyMeta.querySelector('.key-value-display');
if (keyDisplay) {
keyDisplay.textContent = key;
}
if (camelot) {
let camelotDisplay = keyMeta.querySelector('.camelot-value-display');
if (!camelotDisplay) {
camelotDisplay = document.createElement('span');
camelotDisplay.className = 'camelot-value-display';
keyDisplay.parentNode.insertBefore(camelotDisplay, keyDisplay.nextSibling);
}
camelotDisplay.textContent = `(${camelot})`;
}
// Add edit button if it doesn't exist
if (!keyMeta.querySelector('.key-edit-btn')) {
const editBtn = document.createElement('button');
editBtn.className = 'key-edit-btn';
editBtn.onclick = () => openKeyCorrectionModal(trackId, key, camelot);
editBtn.title = 'Edit Key';
editBtn.style.cssText = 'background: none; border: none; color: #667eea; cursor: pointer; margin-left: 4px; padding: 2px 4px;';
editBtn.innerHTML = '<i class="fas fa-edit" style="font-size: 0.75rem;"></i>';
keyMeta.appendChild(editBtn);
}
}
}
// Batch Analyze Tracks
async function batchAnalyzeTracks() {
if (!confirm('This will analyze all tracks that need BPM and key detection. This may take a while. Continue?')) {
return;
}
// Get tracks needing analysis
const response = await fetch('/api/get_analysis_status.php?user_id=<?= $_SESSION['user_id'] ?>');
const data = await response.json();
if (!data.success || !data.tracks || data.tracks.length === 0) {
alert('No tracks need analysis!');
return;
}
const tracks = data.tracks;
let analyzed = 0;
let failed = 0;
let current = 0;
// Create progress modal
const modal = document.createElement('div');
modal.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); z-index: 10000; display: flex; align-items: center; justify-content: center;';
modal.innerHTML = `
<div style="background: #1a1a2e; padding: 2rem; border-radius: 16px; max-width: 500px; width: 90%;">
<h3 style="color: white; margin: 0 0 1rem 0;">Batch Analysis</h3>
<div style="color: rgba(255,255,255,0.8); margin-bottom: 1rem;">
<div>Progress: <span id="batchProgress">0</span> / ${tracks.length}</div>
<div>Analyzed: <span id="batchAnalyzed">0</span></div>
<div>Failed: <span id="batchFailed">0</span></div>
</div>
<div style="background: rgba(255,255,255,0.1); height: 8px; border-radius: 4px; overflow: hidden; margin-bottom: 1rem;">
<div id="batchProgressBar" style="background: #667eea; height: 100%; width: 0%; transition: width 0.3s;"></div>
</div>
<div id="batchCurrentTrack" style="color: rgba(255,255,255,0.6); font-size: 0.9rem; margin-bottom: 1rem;"></div>
<button onclick="this.closest('div[style*=\"position: fixed\"]').remove()" style="background: #667eea; color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 8px; cursor: pointer;">Close</button>
</div>
`;
document.body.appendChild(modal);
// Load simple analyzer script
if (!window.simpleAudioAnalyzer) {
const script = document.createElement('script');
script.src = '/js/audio_analyzer_simple.js';
await new Promise((resolve, reject) => {
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
await new Promise(resolve => setTimeout(resolve, 200));
}
// Analyze each track
for (const track of tracks) {
current++;
document.getElementById('batchCurrentTrack').textContent = `Analyzing: ${track.title}`;
document.getElementById('batchProgress').textContent = current;
document.getElementById('batchProgressBar').style.width = (current / tracks.length * 100) + '%';
try {
// Get signed audio URL via API
const tokenResponse = await fetch(`/api/get_audio_token.php?track_id=${track.id}`);
if (!tokenResponse.ok) {
throw new Error('Failed to get audio token');
}
const tokenData = await tokenResponse.json();
if (!tokenData.success || !tokenData.url) {
throw new Error(tokenData.error || 'Invalid audio URL');
}
let audioUrl = tokenData.url;
if (!audioUrl.startsWith('http')) {
audioUrl = window.location.origin + audioUrl;
}
// Analyze with simple, lightweight analyzer
const result = await window.simpleAudioAnalyzer.analyzeAudio(audioUrl);
if (!result || !result.bpm || !result.key) {
throw new Error('Analysis returned incomplete results');
}
// Save to server
const saveResponse = await fetch('/api/save_audio_analysis.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
track_id: track.id,
bpm: result.bpm,
key: result.key,
camelot: result.camelot || '',
energy: result.energy || 'Medium',
confidence: result.confidence || 50
})
});
const saveData = await saveResponse.json();
if (saveData.success) {
analyzed++;
document.getElementById('batchAnalyzed').textContent = analyzed;
console.log(`✅ Track ${track.id} (${track.title}): BPM=${saveData.data?.bpm || result.bpm}, Key=${saveData.data?.key || result.key}, Saved=${saveData.success}`);
} else {
console.error(`❌ Save failed for track ${track.id}:`, saveData.error);
throw new Error(saveData.error || 'Save failed');
}
} catch (error) {
console.error(`Failed to analyze track ${track.id}:`, error);
failed++;
document.getElementById('batchFailed').textContent = failed;
}
// Delay between tracks to prevent browser blocking
await new Promise(resolve => setTimeout(resolve, 500));
}
// Show completion
document.getElementById('batchCurrentTrack').textContent = `Complete! Analyzed ${analyzed}, Failed ${failed}. Reloading page...`;
// Force a small delay to ensure all saves are complete
await new Promise(resolve => setTimeout(resolve, 1000));
// Reload page to refresh analysis status
setTimeout(() => {
window.location.reload();
}, 2000);
}
// Handle mastered version upload
function handleMasteredUpload(event) {
const input = event.target;
const trackId = input.getAttribute('data-track-id');
const file = input.files[0];
if (!file) return;
// Validate file type - MP3 only
const allowedTypes = ['audio/mpeg', 'audio/mp3'];
if (!allowedTypes.includes(file.type) && !file.name.match(/\.mp3$/i)) {
alert('Please upload an MP3 file only');
input.value = '';
return;
}
// Validate file size (max 50MB)
if (file.size > 50 * 1024 * 1024) {
alert('File size must be less than 50MB');
input.value = '';
return;
}
// Show loading state
const button = input.closest('.action-icon-btn');
const originalHTML = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
button.style.pointerEvents = 'none';
// Create FormData
const formData = new FormData();
formData.append('track_id', trackId);
formData.append('mastered_file', file);
formData.append('action', 'upload_mastered');
// Upload file
fetch('/api/upload_mastered.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Mastered version uploaded successfully!');
// Reload page to show updated version
location.reload();
} else {
alert('Error: ' + (data.error || 'Failed to upload mastered version'));
button.innerHTML = originalHTML;
button.style.pointerEvents = '';
}
})
.catch(error => {
console.error('Upload error:', error);
alert('Error uploading mastered version. Please try again.');
button.innerHTML = originalHTML;
button.style.pointerEvents = '';
})
.finally(() => {
input.value = '';
});
}
document.addEventListener('DOMContentLoaded', function() {
// Handle track image uploads
const uploadInputs = document.querySelectorAll('.track-image-upload-input');
uploadInputs.forEach(input => {
input.addEventListener('change', handleTrackImageUpload);
});
// Handle track image downloads
const downloadButtons = document.querySelectorAll('.track-image-download-btn');
downloadButtons.forEach(btn => {
btn.addEventListener('click', handleTrackImageDownload);
});
// Handle mastered version uploads
const masteredInputs = document.querySelectorAll('.mastered-upload-input');
masteredInputs.forEach(input => {
input.addEventListener('change', handleMasteredUpload);
});
});
// BPM Correction Modal
let currentBPMTrackId = null;
function openBPMCorrectionModal(trackId, currentBPM) {
currentBPMTrackId = trackId;
const modal = document.getElementById('bpmCorrectionModal');
const input = document.getElementById('bpmInput');
if (input) {
input.value = currentBPM || '';
}
if (modal) {
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
setTimeout(() => {
if (input) {
input.focus();
input.select();
}
}, 100);
}
}
function closeBPMCorrectionModal() {
const modal = document.getElementById('bpmCorrectionModal');
if (modal) {
modal.style.display = 'none';
document.body.style.overflow = '';
}
currentBPMTrackId = null;
}
async function saveBPMCorrection() {
const input = document.getElementById('bpmInput');
if (!input || !currentBPMTrackId) return;
const bpmValue = parseFloat(input.value);
if (isNaN(bpmValue) || bpmValue < 40 || bpmValue > 300) {
alert('Please enter a valid BPM between 40 and 300');
return;
}
const roundedBPM = Math.round(bpmValue * 10) / 10;
// Update UI
updateTrackCardBPM(currentBPMTrackId, roundedBPM);
// Get current track data first
const keyMeta = document.getElementById(`key-meta-${currentBPMTrackId}`);
let currentKey = 'C major';
let currentCamelot = '8B';
if (keyMeta) {
const keyDisplay = keyMeta.querySelector('.key-value-display');
const camelotDisplay = keyMeta.querySelector('.camelot-value-display');
if (keyDisplay && keyDisplay.textContent.trim() !== 'Not analyzed') {
currentKey = keyDisplay.textContent.trim();
}
if (camelotDisplay) {
currentCamelot = camelotDisplay.textContent.replace(/[()]/g, '').trim();
}
}
// Save to server
try {
const response = await fetch('/api/save_audio_analysis.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
track_id: currentBPMTrackId,
bpm: roundedBPM,
key: currentKey,
camelot: currentCamelot,
energy: 'Medium',
confidence: 100
})
});
const data = await response.json();
if (data.success) {
showNotification('BPM updated to ' + roundedBPM, 'success');
setTimeout(() => window.location.reload(), 1000);
} else {
throw new Error(data.error || 'Failed to save');
}
} catch (error) {
console.error('Error saving BPM:', error);
showNotification('Failed to save BPM: ' + error.message, 'error');
}
closeBPMCorrectionModal();
}
// Key Correction Modal
const camelotKeys = [
{ key: 'A♭ minor', camelot: '1A', note: 'Abm' },
{ key: 'B major', camelot: '1B', note: 'B' },
{ key: 'E♭ minor', camelot: '2A', note: 'Ebm' },
{ key: 'F# major', camelot: '2B', note: 'F#' },
{ key: 'B♭ minor', camelot: '3A', note: 'Bbm' },
{ key: 'D♭ major', camelot: '3B', note: 'Db' },
{ key: 'F minor', camelot: '4A', note: 'Fm' },
{ key: 'A♭ major', camelot: '4B', note: 'Ab' },
{ key: 'C minor', camelot: '5A', note: 'Cm' },
{ key: 'E♭ major', camelot: '5B', note: 'Eb' },
{ key: 'G minor', camelot: '6A', note: 'Gm' },
{ key: 'B♭ major', camelot: '6B', note: 'Bb' },
{ key: 'D minor', camelot: '7A', note: 'Dm' },
{ key: 'F major', camelot: '7B', note: 'F' },
{ key: 'A minor', camelot: '8A', note: 'Am' },
{ key: 'C major', camelot: '8B', note: 'C' },
{ key: 'E minor', camelot: '9A', note: 'Em' },
{ key: 'G major', camelot: '9B', note: 'G' },
{ key: 'B minor', camelot: '10A', note: 'Bm' },
{ key: 'D major', camelot: '10B', note: 'D' },
{ key: 'F# minor', camelot: '11A', note: 'F#m' },
{ key: 'A major', camelot: '11B', note: 'A' },
{ key: 'D♭ minor', camelot: '12A', note: 'Dbm' },
{ key: 'E major', camelot: '12B', note: 'E' }
];
let currentKeyTrackId = null;
let selectedKeyForCorrection = null;
function openKeyCorrectionModal(trackId, currentKey, currentCamelot) {
currentKeyTrackId = trackId;
const modal = document.getElementById('keyCorrectionModal');
const grid = document.getElementById('keySelectGrid');
if (!modal || !grid) return;
// Populate grid
grid.innerHTML = camelotKeys.map(k => {
const escapedKey = k.key.replace(/'/g, "\\'").replace(/"/g, '"');
return `
<div class="key-option" data-key="${k.key.replace(/"/g, '"')}" data-camelot="${k.camelot}" onclick="selectKey('${escapedKey}', '${k.camelot}')">
<div class="key-name">${k.note}</div>
<div class="camelot">${k.camelot}</div>
<div style="font-size: 0.7rem; color: #888; margin-top: 2px;">${k.key}</div>
</div>
`;
}).join('');
// Highlight current key
if (currentKey || currentCamelot) {
const normalizeKey = (key) => {
if (!key) return '';
return key.replace(/♯/g, '#').replace(/♭/g, 'b')
.replace(/G#/gi, 'Ab').replace(/D#/gi, 'Eb')
.replace(/A#/gi, 'Bb').replace(/C#/gi, 'Db')
.replace(/F#/gi, 'Gb')
.trim();
};
const normalizedCurrentKey = normalizeKey(currentKey);
let currentOption = Array.from(grid.children).find(el =>
el.getAttribute('data-key') === currentKey
);
if (!currentOption) {
currentOption = Array.from(grid.children).find(el => {
const dataKey = el.getAttribute('data-key');
return normalizeKey(dataKey) === normalizedCurrentKey;
});
}
if (!currentOption && currentCamelot) {
currentOption = Array.from(grid.children).find(el =>
el.getAttribute('data-camelot') === currentCamelot
);
}
if (currentOption) {
currentOption.classList.add('selected');
const matchedKey = currentOption.getAttribute('data-key');
const matchedCamelot = currentOption.getAttribute('data-camelot');
selectedKeyForCorrection = { key: matchedKey, camelot: matchedCamelot };
}
}
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function closeKeyCorrectionModal() {
const modal = document.getElementById('keyCorrectionModal');
if (modal) {
modal.style.display = 'none';
document.body.style.overflow = '';
}
currentKeyTrackId = null;
selectedKeyForCorrection = null;
}
function selectKey(key, camelot) {
selectedKeyForCorrection = { key, camelot };
const options = document.querySelectorAll('.key-option');
options.forEach(opt => opt.classList.remove('selected'));
if (event && event.currentTarget) {
event.currentTarget.classList.add('selected');
} else {
const clickedOption = Array.from(options).find(opt =>
opt.getAttribute('data-key') === key && opt.getAttribute('data-camelot') === camelot
);
if (clickedOption) {
clickedOption.classList.add('selected');
}
}
}
async function saveKeyCorrection() {
if (!selectedKeyForCorrection || !currentKeyTrackId) {
alert('Please select a key');
return;
}
// Update UI
updateTrackCardKey(currentKeyTrackId, selectedKeyForCorrection.key, selectedKeyForCorrection.camelot);
// Get current BPM first
const bpmMeta = document.getElementById(`bpm-meta-${currentKeyTrackId}`);
let currentBPM = 120; // Default fallback
if (bpmMeta) {
const bpmDisplay = bpmMeta.querySelector('.bpm-value-display');
if (bpmDisplay) {
const bpmText = bpmDisplay.textContent.trim();
if (bpmText && bpmText !== 'Not analyzed') {
const parsed = parseFloat(bpmText);
if (!isNaN(parsed) && parsed > 0) {
currentBPM = parsed;
}
}
}
}
// Save to server
try {
const response = await fetch('/api/save_audio_analysis.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
track_id: currentKeyTrackId,
bpm: currentBPM,
key: selectedKeyForCorrection.key,
camelot: selectedKeyForCorrection.camelot,
energy: 'Medium',
confidence: 100
})
});
const data = await response.json();
if (data.success) {
showNotification('Key updated to ' + selectedKeyForCorrection.key + ' (' + selectedKeyForCorrection.camelot + ')', 'success');
setTimeout(() => window.location.reload(), 1000);
} else {
throw new Error(data.error || 'Failed to save');
}
} catch (error) {
console.error('Error saving key:', error);
showNotification('Failed to save key: ' + error.message, 'error');
}
closeKeyCorrectionModal();
}
</script>
<!-- BPM Correction Modal -->
<div id="bpmCorrectionModal" class="bpm-correction-modal">
<div class="bpm-correction-content">
<h3><i class="fas fa-tachometer-alt"></i> Correct BPM</h3>
<div class="bpm-input-wrapper">
<label for="bpmInput">Enter the correct BPM:</label>
<input type="number" id="bpmInput" min="40" max="300" step="0.1" placeholder="120" />
<div class="bpm-hint">Valid range: 40-300 BPM (decimals allowed, e.g., 104.2)</div>
</div>
<div style="display: flex; gap: 1rem; justify-content: flex-end;">
<button class="btn btn-secondary" onclick="closeBPMCorrectionModal()">Cancel</button>
<button class="btn btn-primary" onclick="saveBPMCorrection()">Save</button>
</div>
</div>
</div>
<!-- Key Correction Modal -->
<div id="keyCorrectionModal" class="key-correction-modal">
<div class="key-correction-content">
<h3><i class="fas fa-music"></i> Correct Musical Key</h3>
<p style="color: #888; margin-bottom: 0.5rem; font-size: 0.9rem;">Select the correct key for this track:</p>
<p style="color: #667eea; margin-bottom: 1rem; font-size: 0.85rem; font-style: italic;">
<i class="fas fa-info-circle"></i> Format: Key Name (Camelot Code) - e.g., A♭ major (4B), B major (1B)
</p>
<div class="key-select-grid" id="keySelectGrid"></div>
<div style="display: flex; gap: 1rem; justify-content: flex-end; margin-top: 1.5rem;">
<button class="btn btn-secondary" onclick="closeKeyCorrectionModal()">Cancel</button>
<button class="btn btn-primary" onclick="saveKeyCorrection()">Save</button>
</div>
</div>
</div>
<style>
/* Analysis Overlay */
.analysis-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.analysis-overlay.active {
opacity: 1;
pointer-events: all;
}
.analysis-modal {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 16px;
padding: 2rem;
max-width: 400px;
width: 90%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
transform: scale(0.9);
transition: transform 0.3s ease;
}
.analysis-overlay.active .analysis-modal {
transform: scale(1);
}
.analysis-modal-header {
text-align: center;
margin-bottom: 1.5rem;
}
.analysis-modal-header h3 {
color: white;
margin: 0 0 0.5rem 0;
font-size: 1.2rem;
font-weight: 600;
}
.analysis-modal-header p {
color: rgba(255, 255, 255, 0.6);
margin: 0;
font-size: 0.9rem;
}
.analysis-spinner {
width: 60px;
height: 60px;
margin: 0 auto 1.5rem;
position: relative;
}
.analysis-spinner::before {
content: '';
position: absolute;
width: 100%;
height: 100%;
border: 4px solid rgba(102, 126, 234, 0.2);
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.analysis-progress {
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
height: 6px;
overflow: hidden;
margin-bottom: 1rem;
}
.analysis-progress-bar {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
height: 100%;
width: 0%;
transition: width 0.3s ease;
border-radius: 8px;
}
.analysis-status {
text-align: center;
color: rgba(255, 255, 255, 0.8);
font-size: 0.9rem;
min-height: 24px;
}
.analysis-status-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 0;
}
.analysis-status-label {
color: rgba(255, 255, 255, 0.6);
font-size: 0.85rem;
}
.analysis-status-value {
color: white;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.5rem;
}
.analysis-status-value .fa-check {
color: #4ade80;
}
.analysis-status-value .fa-spinner {
color: #667eea;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* BPM Correction Modal */
.bpm-correction-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
z-index: 10002;
display: none;
align-items: center;
justify-content: center;
}
.bpm-correction-content {
background: #2a2a2a;
border-radius: 20px;
padding: 2rem;
max-width: 400px;
width: 90%;
border: 2px solid #667eea;
}
.bpm-correction-content h3 {
margin-bottom: 1.5rem;
color: #667eea;
}
.bpm-input-wrapper {
margin-bottom: 1.5rem;
}
.bpm-input-wrapper label {
display: block;
margin-bottom: 0.5rem;
color: #888;
font-size: 0.9rem;
}
.bpm-input-wrapper input {
width: 100%;
padding: 12px;
background: rgba(255, 255, 255, 0.05);
border: 2px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: white;
font-size: 1.2rem;
text-align: center;
}
.bpm-input-wrapper input:focus {
outline: none;
border-color: #667eea;
}
.bpm-hint {
font-size: 0.85rem;
color: #888;
margin-top: 0.5rem;
text-align: center;
}
/* Key Correction Modal */
.key-correction-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
z-index: 10002;
display: none;
align-items: center;
justify-content: center;
}
.key-correction-content {
background: #2a2a2a;
border-radius: 20px;
padding: 2rem;
max-width: 500px;
width: 90%;
border: 2px solid #667eea;
}
.key-correction-content h3 {
margin-bottom: 1.5rem;
color: #667eea;
}
.key-select-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
margin-bottom: 1rem;
}
.key-option {
background: rgba(255, 255, 255, 0.05);
border: 2px solid rgba(255, 255, 255, 0.1);
padding: 0.75rem;
border-radius: 8px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.key-option:hover {
background: rgba(102, 126, 234, 0.2);
border-color: #667eea;
}
.key-option.selected {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-color: #667eea;
}
.key-option .key-name {
font-weight: 600;
margin-bottom: 0.25rem;
}
.key-option .camelot {
font-size: 0.75rem;
opacity: 0.8;
}
</style>
<?php include 'includes/footer.php'; ?>