![]() 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/.cursor-server/data/User/History/-78897e45/ |
<?php
/**
* Public Crate View Page
* Displays a shared crate with all its tracks
* URL: /crate/123 or /crate.php?id=123
*/
session_start();
require_once 'config/database.php';
require_once 'includes/translations.php';
// Get crate ID from URL
$crate_id = 0;
if (isset($_GET['id'])) {
$crate_id = (int)$_GET['id'];
}
if (!$crate_id) {
header('Location: /community.php');
exit;
}
$pdo = getDBConnection();
// Get crate details (only public crates)
$stmt = $pdo->prepare("
SELECT
ap.id,
ap.name,
ap.description,
ap.user_id,
ap.created_at,
ap.updated_at,
u.name as artist_name,
u.id as artist_id,
up.profile_image
FROM artist_playlists ap
JOIN users u ON ap.user_id = u.id
LEFT JOIN user_profiles up ON u.id = up.user_id
WHERE ap.id = ? AND ap.is_public = 1
");
$stmt->execute([$crate_id]);
$crate = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$crate) {
// Crate not found or is private
header('Location: /community.php');
exit;
}
// Check if is_public column exists in playlist_tracks
$checkColumn = $pdo->query("SHOW COLUMNS FROM playlist_tracks LIKE 'is_public'");
$hasTrackPublicColumn = $checkColumn->rowCount() > 0;
$trackPublicFilter = $hasTrackPublicColumn ? "AND (pt.is_public = 1 OR pt.is_public IS NULL)" : "";
// Get tracks in crate - SECURITY: Never expose audio_url, task_id, or metadata in public view
// Client must use get_audio_token.php API to get signed URLs for playback
$stmt = $pdo->prepare("
SELECT
mt.id,
mt.title,
mt.duration,
mt.price,
u.name as artist_name,
u.id as artist_id,
pt.position
FROM playlist_tracks pt
JOIN music_tracks mt ON pt.track_id = mt.id
JOIN users u ON mt.user_id = u.id
WHERE pt.playlist_id = ? AND mt.status = 'complete' $trackPublicFilter
ORDER BY pt.position ASC
");
$stmt->execute([$crate_id]);
$tracks = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Calculate totals
$total_duration = 0;
$set_duration_minutes = 0;
foreach ($tracks as $track) {
$total_duration += intval($track['duration']);
$trackDuration = intval($track['duration']);
if ($trackDuration >= 300) {
$set_duration_minutes += 2.5;
} else {
$set_duration_minutes += $trackDuration * 0.5 / 60;
}
}
$is_2_hour_set = $set_duration_minutes >= 120;
// Format duration
$hours = floor($total_duration / 3600);
$minutes = floor(($total_duration % 3600) / 60);
$duration_formatted = $hours > 0
? sprintf('%dh %dm', $hours, $minutes)
: sprintf('%dm', $minutes);
// Page metadata
$page_title = htmlspecialchars($crate['name']) . ' - Crate by ' . htmlspecialchars($crate['artist_name']) . ' | SoundStudioPro';
$page_description = 'Listen to ' . htmlspecialchars($crate['name']) . ', a curated crate by ' . htmlspecialchars($crate['artist_name']) . ' with ' . count($tracks) . ' tracks on SoundStudioPro.';
include 'includes/header.php';
?>
<style>
.crate-page {
background: linear-gradient(180deg, #0a0a0a 0%, #1a1a1a 100%);
min-height: 100vh;
padding: 2rem 0 4rem;
}
.crate-page-container {
max-width: 1000px;
margin: 0 auto;
padding: 0 2rem;
}
/* Crate Hero */
.crate-hero {
display: flex;
gap: 3rem;
padding: 3rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 24px;
border: 1px solid rgba(102, 126, 234, 0.2);
margin-bottom: 3rem;
}
.crate-artwork {
width: 220px;
height: 220px;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.4) 0%, rgba(118, 75, 162, 0.4) 100%);
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: relative;
overflow: hidden;
}
.crate-artwork::before {
content: '';
position: absolute;
width: 100%;
height: 100%;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="vinyl" width="20" height="20" patternUnits="userSpaceOnUse"><circle cx="10" cy="10" r="8" fill="none" stroke="rgba(255,255,255,0.15)" stroke-width="0.5"/></pattern></defs><rect width="100" height="100" fill="url(%23vinyl)"/></svg>');
}
.crate-artwork .crate-icon {
font-size: 5rem;
color: white;
z-index: 1;
text-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.crate-details {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.crate-type-label {
font-size: 0.85rem;
color: #667eea;
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 600;
margin-bottom: 0.5rem;
}
.crate-title {
font-size: 3rem;
font-weight: 700;
color: white;
margin: 0 0 1rem 0;
line-height: 1.2;
}
.crate-artist-link {
display: inline-flex;
align-items: center;
gap: 0.75rem;
color: #e0e0e0;
text-decoration: none;
font-size: 1.1rem;
margin-bottom: 1.5rem;
transition: color 0.2s ease;
}
.crate-artist-link:hover {
color: #667eea;
}
.crate-artist-link img {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
}
.crate-artist-link .artist-initial {
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 0.9rem;
}
.crate-stats {
display: flex;
gap: 2rem;
margin-bottom: 1.5rem;
}
.crate-stat {
display: flex;
align-items: center;
gap: 0.5rem;
color: #a0aec0;
font-size: 1rem;
}
.crate-stat i {
color: #667eea;
}
.crate-description {
color: #a0aec0;
font-size: 1.05rem;
line-height: 1.6;
margin-bottom: 1.5rem;
}
/* Set Progress */
.crate-set-progress {
background: rgba(255, 255, 255, 0.05);
padding: 1rem 1.5rem;
border-radius: 12px;
margin-bottom: 1.5rem;
}
.set-progress-bar {
height: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.set-progress-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
}
.set-progress-fill.complete {
background: linear-gradient(90deg, #10b981, #059669);
}
.set-progress-fill.incomplete {
background: linear-gradient(90deg, #f59e0b, #d97706);
}
.set-progress-label {
display: flex;
justify-content: space-between;
font-size: 0.9rem;
color: #a0aec0;
}
.set-progress-label .ready {
color: #10b981;
font-weight: 600;
}
/* Action Buttons */
.crate-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.crate-action-btn {
display: inline-flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 2rem;
border-radius: 12px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.2s ease;
}
.crate-action-btn.primary {
background: linear-gradient(135deg, #10b981, #059669);
color: white;
}
.crate-action-btn.primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(16, 185, 129, 0.3);
}
.crate-action-btn.secondary {
background: rgba(102, 126, 234, 0.15);
color: #667eea;
border: 1px solid rgba(102, 126, 234, 0.3);
}
.crate-action-btn.secondary:hover {
background: rgba(102, 126, 234, 0.25);
}
/* Tracks List */
.crate-tracks-section {
background: rgba(255, 255, 255, 0.03);
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.08);
overflow: hidden;
}
.tracks-header {
padding: 1.5rem 2rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
display: flex;
justify-content: space-between;
align-items: center;
}
.tracks-header h3 {
margin: 0;
color: white;
font-size: 1.3rem;
font-weight: 600;
}
.tracks-header .track-count {
color: #a0aec0;
font-size: 0.95rem;
}
.tracks-list {
padding: 1rem;
}
.track-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 1.5rem;
border-radius: 12px;
transition: background 0.2s ease;
}
.track-item:hover {
background: rgba(255, 255, 255, 0.05);
}
.track-number {
width: 28px;
text-align: center;
color: #666;
font-size: 0.95rem;
font-weight: 500;
}
.track-play-btn {
width: 42px;
height: 42px;
border-radius: 50%;
background: rgba(102, 126, 234, 0.15);
border: none;
color: #667eea;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
font-size: 1rem;
}
.track-play-btn:hover {
background: #667eea;
color: white;
transform: scale(1.1);
}
.track-play-btn.playing {
background: #667eea;
color: white;
box-shadow: 0 0 15px rgba(102, 126, 234, 0.5);
}
.track-info {
flex: 1;
min-width: 0;
}
.track-title {
color: white;
font-weight: 500;
font-size: 1.05rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 0.25rem;
text-decoration: none;
display: block;
transition: color 0.2s ease;
}
.track-title:hover {
color: #667eea;
}
.track-artist {
font-size: 0.9rem;
color: #a0aec0;
text-decoration: none;
display: block;
transition: color 0.2s ease;
}
.track-artist:hover {
color: #667eea;
}
.track-duration {
color: #666;
font-size: 0.95rem;
font-family: monospace;
min-width: 50px;
text-align: right;
}
.track-actions {
display: flex;
gap: 0.5rem;
}
.track-cart-btn {
background: linear-gradient(135deg, #10b981, #059669);
border: none;
color: white;
padding: 0.6rem 1.2rem;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.4rem;
transition: all 0.2s ease;
}
.track-cart-btn:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}
/* Responsive */
@media (max-width: 768px) {
.crate-hero {
flex-direction: column;
text-align: center;
padding: 2rem;
}
.crate-artwork {
width: 180px;
height: 180px;
margin: 0 auto;
}
.crate-title {
font-size: 2rem;
}
.crate-stats {
justify-content: center;
flex-wrap: wrap;
}
.crate-artist-link {
justify-content: center;
}
.crate-actions {
justify-content: center;
}
.track-item {
flex-wrap: wrap;
gap: 0.75rem;
}
.track-actions {
width: 100%;
margin-top: 0.5rem;
justify-content: flex-end;
}
}
</style>
<div class="crate-page">
<div class="crate-page-container">
<!-- Crate Hero -->
<div class="crate-hero">
<div class="crate-artwork">
<div class="crate-icon">📦</div>
</div>
<div class="crate-details">
<div class="crate-type-label">Public Crate</div>
<h1 class="crate-title"><?= htmlspecialchars($crate['name']) ?></h1>
<a href="/artist/<?= $crate['artist_id'] ?>" class="crate-artist-link">
<?php if (!empty($crate['profile_image'])): ?>
<img src="<?= htmlspecialchars($crate['profile_image']) ?>" alt="<?= htmlspecialchars($crate['artist_name']) ?>">
<?php else: ?>
<span class="artist-initial"><?= strtoupper(substr($crate['artist_name'], 0, 1)) ?></span>
<?php endif; ?>
<span><?= htmlspecialchars($crate['artist_name']) ?></span>
</a>
<div class="crate-stats">
<div class="crate-stat">
<i class="fas fa-music"></i>
<span><?= count($tracks) ?> tracks</span>
</div>
<div class="crate-stat">
<i class="fas fa-clock"></i>
<span><?= $duration_formatted ?></span>
</div>
</div>
<?php if (!empty($crate['description'])): ?>
<p class="crate-description"><?= htmlspecialchars($crate['description']) ?></p>
<?php endif; ?>
<!-- Actions -->
<div class="crate-actions">
<button class="crate-action-btn primary" onclick="playAllTracks()">
<i class="fas fa-play"></i>
Play All
</button>
<button class="crate-action-btn secondary" onclick="shareCrate()">
<i class="fas fa-share-alt"></i>
Share
</button>
</div>
</div>
</div>
<!-- Tracks List -->
<div class="crate-tracks-section">
<div class="tracks-header">
<h3><i class="fas fa-list"></i> Tracks</h3>
<span class="track-count"><?= count($tracks) ?> tracks</span>
</div>
<div class="tracks-list">
<?php if (empty($tracks)): ?>
<div style="text-align: center; padding: 3rem; color: #a0aec0;">
<div style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.5;">🎵</div>
<p>No tracks in this crate yet.</p>
</div>
<?php else: ?>
<?php foreach ($tracks as $index => $track):
$trackDuration = intval($track['duration']);
$mins = floor($trackDuration / 60);
$secs = $trackDuration % 60;
$durationFormatted = sprintf('%d:%02d', $mins, $secs);
$price = floatval($track['price'] ?? 0);
?>
<div class="track-item" data-track-id="<?= $track['id'] ?>" data-index="<?= $index ?>">
<div class="track-number"><?= $index + 1 ?></div>
<button class="track-play-btn" data-index="<?= $index ?>" onclick="playTrackSecure(<?= $index ?>, this)">
<i class="fas fa-play"></i>
</button>
<div class="track-info">
<a href="/track/<?= $track['id'] ?>" class="track-title"><?= htmlspecialchars($track['title'] ?? 'Untitled') ?></a>
<a href="/artist/<?= $track['artist_id'] ?>" class="track-artist"><?= htmlspecialchars($track['artist_name']) ?></a>
</div>
<div class="track-duration"><?= $durationFormatted ?></div>
<div class="track-actions">
<?php if ($price > 0): ?>
<button class="track-cart-btn" onclick="addToCart(<?= $track['id'] ?>, '<?= htmlspecialchars($track['title']) ?>', <?= $price ?>)">
<i class="fas fa-cart-plus"></i>
$<?= number_format($price, 2) ?>
</button>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
</div>
<script>
// SECURITY: Track metadata only - NO audio URLs exposed
// Audio URLs are fetched via signed tokens when needed
const crateTracks = <?= json_encode(array_map(function($t) {
return [
'id' => $t['id'],
'title' => $t['title'] ?? 'Untitled',
'artist_name' => $t['artist_name'],
'artist_id' => $t['artist_id'],
'duration' => $t['duration']
];
}, $tracks)) ?>;
let currentPlayingIndex = -1;
// Secure play - fetches signed token before playing
async function playTrackSecure(index, button) {
if (!crateTracks[index]) return;
const track = crateTracks[index];
const trackId = track.id;
const title = track.title;
const artist = track.artist_name;
const artistId = track.artist_id;
const duration = track.duration || 300;
// Show loading state
if (button) {
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
button.disabled = true;
}
try {
// Fetch signed audio token from API
const response = await fetch(`/api/get_audio_token.php?track_id=${trackId}&duration=${duration}`);
const data = await response.json();
if (!data.success || !data.url) {
throw new Error(data.error || 'Failed to get audio');
}
const signedUrl = data.url;
currentPlayingIndex = index;
// Update button to pause
updatePlayButtons(index, true);
if (button) button.disabled = false;
// Play using global player
if (window.enhancedGlobalPlayer && typeof window.enhancedGlobalPlayer.playTrack === 'function') {
window.enhancedGlobalPlayer.playTrack(signedUrl, title, artist, trackId, artistId);
} else if (typeof window.playTrack === 'function') {
window.playTrack(signedUrl, title, artist, trackId, artistId);
} else {
// Fallback to basic audio
const audio = document.getElementById('globalAudioElement');
if (audio) {
audio.src = signedUrl;
audio.play();
}
}
// Set up event listeners for button sync
const audioElement = document.getElementById('globalAudioElement');
if (audioElement) {
audioElement.onpause = () => updatePlayButtons(currentPlayingIndex, false);
audioElement.onplay = () => updatePlayButtons(currentPlayingIndex, true);
audioElement.onended = () => {
updatePlayButtons(-1, false);
// Auto-play next track
if (currentPlayingIndex < crateTracks.length - 1) {
playTrackSecure(currentPlayingIndex + 1, null);
}
};
}
} catch (error) {
console.error('Error playing track:', error);
if (button) {
button.innerHTML = '<i class="fas fa-play"></i>';
button.disabled = false;
}
if (typeof showNotification === 'function') {
showNotification('Error loading track. Please try again.', 'error');
}
}
}
function updatePlayButtons(playingIndex, isPlaying) {
document.querySelectorAll('.track-play-btn').forEach((btn, idx) => {
const btnIndex = parseInt(btn.getAttribute('data-index'));
if (btnIndex === playingIndex && isPlaying) {
btn.innerHTML = '<i class="fas fa-pause"></i>';
btn.classList.add('playing');
} else {
btn.innerHTML = '<i class="fas fa-play"></i>';
btn.classList.remove('playing');
}
});
}
function playAllTracks() {
if (crateTracks.length === 0) return;
// Start playing from first track
playTrackSecure(0, document.querySelector('.track-play-btn[data-index="0"]'));
}
function shareCrate() {
const url = window.location.href;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url)
.then(() => {
if (typeof showNotification === 'function') {
showNotification('Crate link copied to clipboard!', 'success');
} else {
alert('Link copied!');
}
});
} else {
const textarea = document.createElement('textarea');
textarea.value = url;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
alert('Link copied!');
}
}
function addToCart(trackId, title, price) {
const formData = new FormData();
formData.append('action', 'add');
formData.append('track_id', trackId);
fetch('/cart.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof showNotification === 'function') {
showNotification(`"${title}" added to cart!`, 'success');
} else {
alert('Added to cart!');
}
} else {
if (typeof showNotification === 'function') {
showNotification(data.error || 'Failed to add to cart', 'error');
}
}
})
.catch(error => {
console.error('Cart error:', error);
});
}
</script>
<?php include 'includes/footer.php'; ?>