![]() 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-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;
}
.track-artist {
font-size: 0.9rem;
color: #a0aec0;
}
.track-artist a {
color: #a0aec0;
text-decoration: none;
}
.track-artist a: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'] ?>">
<div class="track-number"><?= $index + 1 ?></div>
<button class="track-play-btn" onclick="playTrack(<?= $track['id'] ?>, '<?= htmlspecialchars($track['audio_url'] ?? '') ?>', '<?= htmlspecialchars($track['title'] ?? 'Untitled') ?>')">
<i class="fas fa-play"></i>
</button>
<div class="track-info">
<div class="track-title"><?= htmlspecialchars($track['title'] ?? 'Untitled') ?></div>
<div class="track-artist">
<a href="/artist/<?= $track['artist_id'] ?>"><?= htmlspecialchars($track['artist_name']) ?></a>
</div>
</div>
<div class="track-duration"><?= $durationFormatted ?></div>
<div class="track-actions">
<?php if ($price > 0 && !empty($track['is_public'])): ?>
<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>
const crateTracks = <?= json_encode(array_map(function($t) {
return [
'id' => $t['id'],
'title' => $t['title'] ?? 'Untitled',
'audio_url' => $t['audio_url'] ?? '',
'artist_name' => $t['artist_name'],
'duration' => $t['duration']
];
}, $tracks)) ?>;
function playTrack(trackId, audioUrl, title) {
if (typeof window.playTrack === 'function') {
window.playTrack(trackId, audioUrl, title);
} else if (typeof window.audioPlayer !== 'undefined') {
window.audioPlayer.src = audioUrl;
window.audioPlayer.play();
} else {
console.warn('No audio player available');
}
}
function playAllTracks() {
if (crateTracks.length === 0) return;
if (typeof window.playPlaylist === 'function') {
window.playPlaylist(crateTracks);
} else {
// Play first track
playTrack(crateTracks[0].id, crateTracks[0].audio_url, crateTracks[0].title);
}
}
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!');
}
}
</script>
<?php include 'includes/footer.php'; ?>