![]() 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
/**
* Batch Audio Analysis - Admin Tool
* Analyzes all tracks without analyzed metadata using client-side Web Audio API
*/
require_once 'config/database.php';
require_once 'utils/audio_token.php';
session_start();
// Check admin access
if (!isset($_SESSION['user_id'])) {
header('Location: /auth/login.php');
exit;
}
// Check if user is admin (you may need to adjust this check)
$pdo = getDBConnection();
$stmt = $pdo->prepare("SELECT plan FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// For now, allow any logged-in user (you can restrict to admins only)
// if ($user['plan'] !== 'admin') {
// die('Access denied');
// }
// Get batch size from query param (default 50 to avoid browser timeout)
$batchSize = isset($_GET['batch_size']) ? intval($_GET['batch_size']) : 50;
$offset = isset($_GET['offset']) ? intval($_GET['offset']) : 0;
// Get tracks that need analysis
$stmt = $pdo->prepare("
SELECT
mt.id,
mt.title,
mt.audio_url,
mt.metadata,
u.name as artist_name
FROM music_tracks mt
JOIN users u ON mt.user_id = u.id
WHERE mt.status = 'complete'
AND mt.audio_url IS NOT NULL
AND mt.audio_url != ''
AND (
JSON_EXTRACT(mt.metadata, '$.analyzed_bpm') IS NULL
OR JSON_EXTRACT(mt.metadata, '$.analysis_source') IS NULL
)
ORDER BY mt.created_at DESC
LIMIT ? OFFSET ?
");
$stmt->execute([$batchSize, $offset]);
$tracksRaw = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Get total count for display
$countStmt = $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 != ''
AND (
JSON_EXTRACT(mt.metadata, '$.analyzed_bpm') IS NULL
OR JSON_EXTRACT(mt.metadata, '$.analysis_source') IS NULL
)
");
$countStmt->execute();
$totalCount = $countStmt->fetchColumn();
// Generate signed URLs for each track
$tracksToAnalyze = [];
foreach ($tracksRaw as $track) {
$signedUrl = getSignedAudioUrl($track['id'], null, 3600, $_SESSION['user_id'], session_id());
$tracksToAnalyze[] = [
'id' => $track['id'],
'title' => $track['title'],
'audio_url' => $signedUrl,
'artist_name' => $track['artist_name']
];
}
$totalTracks = count($tracksToAnalyze);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Batch Audio Analysis - SoundStudioPro</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #1a1a1a;
color: #fff;
padding: 2rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
margin-bottom: 2rem;
color: #667eea;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: rgba(255, 255, 255, 0.05);
padding: 1.5rem;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.stat-label {
font-size: 0.9rem;
color: #888;
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #667eea;
}
.controls {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.progress-bar {
width: 100%;
height: 30px;
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
overflow: hidden;
margin-bottom: 1rem;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
transition: width 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 0.9rem;
}
.log {
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
padding: 1rem;
max-height: 400px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
margin-top: 2rem;
}
.log-entry {
padding: 0.25rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.log-entry.success {
color: #48bb78;
}
.log-entry.error {
color: #ef4444;
}
.log-entry.info {
color: #667eea;
}
.current-track {
background: rgba(102, 126, 234, 0.1);
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
border-left: 4px solid #667eea;
}
</style>
</head>
<body>
<div class="container">
<h1><i class="fas fa-wave-square"></i> Batch Audio Analysis</h1>
<p style="color: #888; margin-bottom: 2rem;">
This tool analyzes tracks using your browser's Web Audio API.
Processing <?= $totalTracks ?> tracks in this batch (<?= $totalCount ?> total needing analysis).
<?php if ($offset > 0): ?>
<br><a href="?offset=<?= max(0, $offset - $batchSize) ?>&batch_size=<?= $batchSize ?>" style="color: #667eea;">← Previous Batch</a>
<?php endif; ?>
<?php if ($offset + $batchSize < $totalCount): ?>
<a href="?offset=<?= $offset + $batchSize ?>&batch_size=<?= $batchSize ?>" style="color: #667eea; margin-left: 1rem;">Next Batch →</a>
<?php endif; ?>
</p>
<div class="stats">
<div class="stat-card">
<div class="stat-label">Total Needing Analysis</div>
<div class="stat-value" id="totalTracks"><?= $totalCount ?></div>
</div>
<div class="stat-card">
<div class="stat-label">In This Batch</div>
<div class="stat-value" id="batchSize"><?= $totalTracks ?></div>
</div>
<div class="stat-card">
<div class="stat-label">Analyzed</div>
<div class="stat-value" id="analyzedCount">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Failed</div>
<div class="stat-value" id="failedCount">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Progress</div>
<div class="stat-value" id="progressPercent">0%</div>
</div>
</div>
<div class="controls">
<button class="btn btn-primary" id="startBtn" onclick="startAnalysis()">
<i class="fas fa-play"></i> Start Analysis
</button>
<button class="btn btn-secondary" id="pauseBtn" onclick="pauseAnalysis()" disabled>
<i class="fas fa-pause"></i> Pause
</button>
<button class="btn btn-secondary" id="stopBtn" onclick="stopAnalysis()" disabled>
<i class="fas fa-stop"></i> Stop
</button>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progressFill" style="width: 0%">0%</div>
</div>
<div class="current-track" id="currentTrack" style="display: none;">
<strong>Analyzing:</strong> <span id="currentTrackName">-</span>
</div>
<div class="log" id="log"></div>
</div>
<script src="/js/audio_analyzer_simple.js?v=<?= time() ?>"></script>
<script>
const tracks = <?= json_encode($tracksToAnalyze) ?>;
let currentIndex = 0;
let isRunning = false;
let isPaused = false;
let analyzedCount = 0;
let failedCount = 0;
// Ensure simple analyzer is loaded
if (!window.simpleAudioAnalyzer) {
log('⚠️ Waiting for analyzer to load...', 'info');
const checkInterval = setInterval(() => {
if (window.simpleAudioAnalyzer) {
clearInterval(checkInterval);
log('✅ Analyzer loaded', 'success');
}
}, 100);
}
function log(message, type = 'info') {
const logDiv = document.getElementById('log');
const entry = document.createElement('div');
entry.className = `log-entry ${type}`;
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
logDiv.appendChild(entry);
logDiv.scrollTop = logDiv.scrollHeight;
}
function updateStats() {
const total = tracks.length;
const progress = total > 0 ? Math.round((analyzedCount + failedCount) / total * 100) : 0;
document.getElementById('analyzedCount').textContent = analyzedCount;
document.getElementById('failedCount').textContent = failedCount;
document.getElementById('progressPercent').textContent = progress + '%';
document.getElementById('progressFill').style.width = progress + '%';
document.getElementById('progressFill').textContent = progress + '%';
}
async function analyzeNextTrack() {
if (currentIndex >= tracks.length) {
stopAnalysis();
log('✅ All tracks analyzed!', 'success');
return;
}
if (isPaused) {
return;
}
const track = tracks[currentIndex];
currentIndex++;
// Show current track
document.getElementById('currentTrack').style.display = 'block';
document.getElementById('currentTrackName').textContent =
`${track.title} by ${track.artist_name} (Track #${track.id})`;
log(`Analyzing track ${currentIndex}/${tracks.length}: ${track.title}`, 'info');
try {
// Get signed audio URL via API (more secure)
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;
}
// Ensure simple analyzer is available
if (!window.simpleAudioAnalyzer) {
throw new Error('Audio analyzer not loaded');
}
// Analyze with improved simple analyzer (better BPM/key detection)
const result = await window.simpleAudioAnalyzer.analyzeAudio(audioUrl);
if (result) {
// 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,
confidence: result.confidence || 80
})
});
const saveData = await saveResponse.json();
if (saveData.success) {
analyzedCount++;
// Log what was actually saved (from server response)
const savedBpm = saveData.data?.bpm ?? result.bpm;
const savedKey = saveData.data?.key ?? result.key;
const savedCamelot = saveData.data?.camelot ?? result.camelot;
log(`✅ ${track.title}: BPM=${savedBpm}, Key=${savedKey} (${savedCamelot}), Energy=${result.energy}`, 'success');
// Notify track.php page if it's open to update the display
window.postMessage({
type: 'analysis_complete',
trackId: track.id,
bpm: savedBpm,
key: savedKey,
camelot: savedCamelot
}, '*');
} else {
failedCount++;
log(`❌ Failed to save: ${saveData.error || 'Unknown error'}`, 'error');
console.error('Save error details:', saveData);
}
} else {
failedCount++;
log(`❌ Analysis returned no result`, 'error');
}
} catch (error) {
failedCount++;
log(`❌ Error analyzing ${track.title}: ${error.message}`, 'error');
}
updateStats();
// Continue to next track (with small delay to prevent browser freezing)
if (isRunning) {
setTimeout(analyzeNextTrack, 500);
}
}
function startAnalysis() {
if (isRunning) return;
isRunning = true;
isPaused = false;
document.getElementById('startBtn').disabled = true;
document.getElementById('pauseBtn').disabled = false;
document.getElementById('stopBtn').disabled = false;
log('🚀 Starting batch analysis...', 'info');
analyzeNextTrack();
}
function pauseAnalysis() {
isPaused = !isPaused;
document.getElementById('pauseBtn').textContent = isPaused ?
'<i class="fas fa-play"></i> Resume' :
'<i class="fas fa-pause"></i> Pause';
if (!isPaused) {
analyzeNextTrack();
}
}
function stopAnalysis() {
isRunning = false;
isPaused = false;
document.getElementById('startBtn').disabled = false;
document.getElementById('pauseBtn').disabled = true;
document.getElementById('stopBtn').disabled = true;
document.getElementById('currentTrack').style.display = 'none';
log('⏹️ Analysis stopped', 'info');
}
// Initialize
updateStats();
log(`Ready to analyze ${tracks.length} tracks`, 'info');
</script>
</body>
</html>