download missing tracks modal
This commit is contained in:
parent
584245f8a4
commit
632f45c68e
3 changed files with 1025 additions and 0 deletions
199
web_server.py
199
web_server.py
|
|
@ -2387,6 +2387,205 @@ def stop_database_update():
|
||||||
else:
|
else:
|
||||||
return jsonify({"success": False, "error": "No update is currently running."}), 404
|
return jsonify({"success": False, "error": "No update is currently running."}), 404
|
||||||
|
|
||||||
|
# ===============================
|
||||||
|
# == TRACK ANALYSIS API ==
|
||||||
|
# ===============================
|
||||||
|
|
||||||
|
# Global state for track analysis tasks
|
||||||
|
analysis_executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="AnalysisWorker")
|
||||||
|
analysis_tasks = {} # task_id -> analysis state
|
||||||
|
analysis_lock = threading.Lock()
|
||||||
|
|
||||||
|
def _run_track_analysis_task(task_id, tracks_json):
|
||||||
|
"""Run track analysis in background thread (same logic as GUI's PlaylistTrackAnalysisWorker)"""
|
||||||
|
import uuid
|
||||||
|
from database.music_database import MusicDatabase
|
||||||
|
from config.settings import config_manager
|
||||||
|
|
||||||
|
print(f"🔍 Starting track analysis task {task_id} for {len(tracks_json)} tracks")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Initialize database connection
|
||||||
|
db = MusicDatabase()
|
||||||
|
active_server = config_manager.get_active_media_server()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
total_tracks = len(tracks_json)
|
||||||
|
|
||||||
|
for i, track_data in enumerate(tracks_json):
|
||||||
|
with analysis_lock:
|
||||||
|
# Check if task was cancelled
|
||||||
|
if analysis_tasks.get(task_id, {}).get('status') == 'cancelled':
|
||||||
|
print(f"❌ Analysis task {task_id} was cancelled")
|
||||||
|
return
|
||||||
|
|
||||||
|
track_name = track_data.get('name', '')
|
||||||
|
artists = track_data.get('artists', [])
|
||||||
|
|
||||||
|
# Try each artist for matching (same as GUI logic)
|
||||||
|
found = False
|
||||||
|
confidence = 0.0
|
||||||
|
|
||||||
|
for artist in artists:
|
||||||
|
artist_name = artist if isinstance(artist, str) else str(artist)
|
||||||
|
|
||||||
|
# Check database for track existence
|
||||||
|
db_track, track_confidence = db.check_track_exists(
|
||||||
|
track_name, artist_name,
|
||||||
|
confidence_threshold=0.7,
|
||||||
|
server_source=active_server
|
||||||
|
)
|
||||||
|
|
||||||
|
if db_track and track_confidence >= 0.7:
|
||||||
|
found = True
|
||||||
|
confidence = track_confidence
|
||||||
|
print(f"✅ Found: '{track_name}' by {artist_name} (confidence: {confidence:.2f})")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
print(f"❌ Missing: '{track_name}' by {artists}")
|
||||||
|
|
||||||
|
# Store result
|
||||||
|
result = {
|
||||||
|
'track_index': i,
|
||||||
|
'track': track_data,
|
||||||
|
'found': found,
|
||||||
|
'confidence': confidence
|
||||||
|
}
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
# Update progress
|
||||||
|
progress = int((i + 1) / total_tracks * 100)
|
||||||
|
with analysis_lock:
|
||||||
|
if task_id in analysis_tasks:
|
||||||
|
analysis_tasks[task_id].update({
|
||||||
|
'progress': progress,
|
||||||
|
'processed': i + 1,
|
||||||
|
'results': results.copy() # Store current results
|
||||||
|
})
|
||||||
|
|
||||||
|
# Mark as complete
|
||||||
|
with analysis_lock:
|
||||||
|
if task_id in analysis_tasks:
|
||||||
|
analysis_tasks[task_id].update({
|
||||||
|
'status': 'complete',
|
||||||
|
'progress': 100,
|
||||||
|
'results': results,
|
||||||
|
'total_found': len([r for r in results if r['found']]),
|
||||||
|
'total_missing': len([r for r in results if not r['found']])
|
||||||
|
})
|
||||||
|
|
||||||
|
print(f"✅ Analysis complete: {len([r for r in results if r['found']])} found, {len([r for r in results if not r['found']])} missing")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Analysis task {task_id} failed: {e}")
|
||||||
|
with analysis_lock:
|
||||||
|
if task_id in analysis_tasks:
|
||||||
|
analysis_tasks[task_id].update({
|
||||||
|
'status': 'error',
|
||||||
|
'error': str(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route('/api/tracks/analyze', methods=['POST'])
|
||||||
|
def start_track_analysis():
|
||||||
|
"""Start track analysis to check which tracks exist in media server library"""
|
||||||
|
data = request.get_json()
|
||||||
|
tracks = data.get('tracks', [])
|
||||||
|
|
||||||
|
if not tracks:
|
||||||
|
return jsonify({"success": False, "error": "No tracks provided"}), 400
|
||||||
|
|
||||||
|
# Generate unique task ID
|
||||||
|
import uuid
|
||||||
|
task_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Initialize task state
|
||||||
|
with analysis_lock:
|
||||||
|
analysis_tasks[task_id] = {
|
||||||
|
'status': 'running',
|
||||||
|
'progress': 0,
|
||||||
|
'total': len(tracks),
|
||||||
|
'processed': 0,
|
||||||
|
'results': [],
|
||||||
|
'total_found': 0,
|
||||||
|
'total_missing': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Submit analysis task
|
||||||
|
future = analysis_executor.submit(_run_track_analysis_task, task_id, tracks)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"task_id": task_id,
|
||||||
|
"total_tracks": len(tracks)
|
||||||
|
})
|
||||||
|
|
||||||
|
@app.route('/api/tracks/analyze/status/<task_id>', methods=['GET'])
|
||||||
|
def get_analysis_status(task_id):
|
||||||
|
"""Get status of track analysis task"""
|
||||||
|
with analysis_lock:
|
||||||
|
task = analysis_tasks.get(task_id)
|
||||||
|
if not task:
|
||||||
|
return jsonify({"error": "Task not found"}), 404
|
||||||
|
|
||||||
|
return jsonify(task)
|
||||||
|
|
||||||
|
@app.route('/api/tracks/analyze/cancel/<task_id>', methods=['POST'])
|
||||||
|
def cancel_analysis_task(task_id):
|
||||||
|
"""Cancel a running analysis task"""
|
||||||
|
with analysis_lock:
|
||||||
|
if task_id in analysis_tasks:
|
||||||
|
analysis_tasks[task_id]['status'] = 'cancelled'
|
||||||
|
return jsonify({"success": True, "message": "Task cancelled"})
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "error": "Task not found"}), 404
|
||||||
|
|
||||||
|
@app.route('/api/tracks/download_missing', methods=['POST'])
|
||||||
|
def start_missing_downloads():
|
||||||
|
"""Queue missing tracks for Soulseek download"""
|
||||||
|
data = request.get_json()
|
||||||
|
missing_tracks = data.get('missing_tracks', [])
|
||||||
|
|
||||||
|
if not missing_tracks:
|
||||||
|
return jsonify({"success": False, "error": "No missing tracks provided"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
queued_downloads = 0
|
||||||
|
|
||||||
|
for track_data in missing_tracks:
|
||||||
|
track = track_data.get('track', {})
|
||||||
|
track_name = track.get('name', '')
|
||||||
|
artists = track.get('artists', [])
|
||||||
|
|
||||||
|
if not track_name or not artists:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Generate search query (simplified version of GUI logic)
|
||||||
|
artist_name = artists[0] if artists else 'Unknown Artist'
|
||||||
|
search_query = f"{track_name} {artist_name}".strip()
|
||||||
|
|
||||||
|
print(f"📥 Queuing download: '{search_query}'")
|
||||||
|
|
||||||
|
# Queue download using existing soulseek client
|
||||||
|
try:
|
||||||
|
asyncio.run(soulseek_client.queue_search_and_download(
|
||||||
|
query=search_query,
|
||||||
|
preferred_format='flac' # Use user's preferred format
|
||||||
|
))
|
||||||
|
queued_downloads += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Failed to queue download for '{search_query}': {e}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"queued": queued_downloads,
|
||||||
|
"message": f"Queued {queued_downloads} downloads"
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error queueing downloads: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
# ===============================
|
# ===============================
|
||||||
# == SYNC PAGE API ==
|
# == SYNC PAGE API ==
|
||||||
# ===============================
|
# ===============================
|
||||||
|
|
|
||||||
|
|
@ -1578,6 +1578,7 @@ function showPlaylistDetailsModal(playlist) {
|
||||||
|
|
||||||
<div class="playlist-modal-footer">
|
<div class="playlist-modal-footer">
|
||||||
<button class="playlist-modal-btn playlist-modal-btn-secondary" onclick="closePlaylistDetailsModal()">Close</button>
|
<button class="playlist-modal-btn playlist-modal-btn-secondary" onclick="closePlaylistDetailsModal()">Close</button>
|
||||||
|
<button class="playlist-modal-btn playlist-modal-btn-tertiary" onclick="openDownloadMissingModal('${playlist.id}')">📥 Download Missing Tracks</button>
|
||||||
<button class="playlist-modal-btn playlist-modal-btn-primary" onclick="startPlaylistSync('${playlist.id}')">Sync Playlist</button>
|
<button class="playlist-modal-btn playlist-modal-btn-primary" onclick="startPlaylistSync('${playlist.id}')">Sync Playlist</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1599,6 +1600,466 @@ function formatDuration(ms) {
|
||||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===============================
|
||||||
|
// DOWNLOAD MISSING TRACKS MODAL
|
||||||
|
// ===============================
|
||||||
|
|
||||||
|
let activeAnalysisTaskId = null;
|
||||||
|
let currentPlaylistTracks = [];
|
||||||
|
let analysisResults = [];
|
||||||
|
let missingTracks = [];
|
||||||
|
|
||||||
|
async function openDownloadMissingModal(playlistId) {
|
||||||
|
console.log(`📥 Opening Download Missing Tracks modal for playlist: ${playlistId}`);
|
||||||
|
|
||||||
|
// Close the playlist details modal first
|
||||||
|
closePlaylistDetailsModal();
|
||||||
|
|
||||||
|
// Find playlist data
|
||||||
|
const playlist = spotifyPlaylists.find(p => p.id === playlistId);
|
||||||
|
if (!playlist) {
|
||||||
|
console.error(`❌ Could not find playlist data for ID: ${playlistId}`);
|
||||||
|
showToast('Could not find playlist data.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we have track data
|
||||||
|
let tracks = playlistTrackCache[playlistId];
|
||||||
|
if (!tracks) {
|
||||||
|
console.log(`🔄 Cache miss - fetching tracks for download analysis`);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/spotify/playlist/${playlistId}`);
|
||||||
|
const fullPlaylist = await response.json();
|
||||||
|
if (fullPlaylist.error) throw new Error(fullPlaylist.error);
|
||||||
|
tracks = fullPlaylist.tracks;
|
||||||
|
playlistTrackCache[playlistId] = tracks; // Cache it
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Failed to fetch tracks:`, error);
|
||||||
|
showToast(`Failed to fetch tracks: ${error.message}`, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPlaylistTracks = tracks;
|
||||||
|
console.log(`✅ Loaded ${tracks.length} tracks for analysis`);
|
||||||
|
|
||||||
|
// Create or get modal
|
||||||
|
let modal = document.getElementById('download-missing-modal');
|
||||||
|
if (!modal) {
|
||||||
|
modal = document.createElement('div');
|
||||||
|
modal.id = 'download-missing-modal';
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build modal HTML
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="download-missing-modal-content">
|
||||||
|
<div class="download-missing-modal-header">
|
||||||
|
<h2 class="download-missing-modal-title">Download Missing Tracks - ${escapeHtml(playlist.name)}</h2>
|
||||||
|
<span class="download-missing-modal-close" onclick="closeDownloadMissingModal()">×</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="download-missing-modal-body">
|
||||||
|
<!-- Dashboard Stats -->
|
||||||
|
<div class="download-dashboard-stats">
|
||||||
|
<div class="dashboard-stat stat-total">
|
||||||
|
<div class="dashboard-stat-number" id="stat-total">${tracks.length}</div>
|
||||||
|
<div class="dashboard-stat-label">Total Tracks</div>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-stat stat-found">
|
||||||
|
<div class="dashboard-stat-number" id="stat-found">-</div>
|
||||||
|
<div class="dashboard-stat-label">Found in Library</div>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-stat stat-missing">
|
||||||
|
<div class="dashboard-stat-number" id="stat-missing">-</div>
|
||||||
|
<div class="dashboard-stat-label">Missing Tracks</div>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-stat stat-downloaded">
|
||||||
|
<div class="dashboard-stat-number" id="stat-downloaded">0</div>
|
||||||
|
<div class="dashboard-stat-label">Downloaded</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress Section -->
|
||||||
|
<div class="download-progress-section">
|
||||||
|
<div class="progress-item">
|
||||||
|
<div class="progress-label">
|
||||||
|
🔍 Library Analysis
|
||||||
|
<span id="analysis-progress-text">Ready to start</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill analysis" id="analysis-progress-fill"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-item">
|
||||||
|
<div class="progress-label">
|
||||||
|
⏬ Downloads
|
||||||
|
<span id="download-progress-text">Waiting for analysis</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill download" id="download-progress-fill"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Track Table -->
|
||||||
|
<div class="download-tracks-section">
|
||||||
|
<div class="download-tracks-header">
|
||||||
|
<h3 class="download-tracks-title">📋 Track Analysis & Download Status</h3>
|
||||||
|
</div>
|
||||||
|
<div class="download-tracks-table-container">
|
||||||
|
<table class="download-tracks-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Track</th>
|
||||||
|
<th>Artist</th>
|
||||||
|
<th>Duration</th>
|
||||||
|
<th>Library Match</th>
|
||||||
|
<th>Download Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="download-tracks-tbody">
|
||||||
|
${tracks.map((track, index) => `
|
||||||
|
<tr data-track-index="${index}">
|
||||||
|
<td class="track-number">${index + 1}</td>
|
||||||
|
<td class="track-name">${escapeHtml(track.name)}</td>
|
||||||
|
<td class="track-artist">${track.artists.join(', ')}</td>
|
||||||
|
<td class="track-duration">${formatDuration(track.duration_ms)}</td>
|
||||||
|
<td class="track-match-status match-checking" id="match-${index}">🔍 Pending</td>
|
||||||
|
<td class="track-download-status" id="download-${index}">-</td>
|
||||||
|
<td class="track-actions" id="actions-${index}">-</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="download-missing-modal-footer">
|
||||||
|
<div class="download-phase-controls">
|
||||||
|
<button class="download-control-btn primary" id="begin-analysis-btn" onclick="startTrackAnalysis()">
|
||||||
|
Begin Analysis
|
||||||
|
</button>
|
||||||
|
<button class="download-control-btn primary" id="start-downloads-btn" onclick="startMissingDownloads()" style="display: none;">
|
||||||
|
Start Downloads
|
||||||
|
</button>
|
||||||
|
<button class="download-control-btn danger" id="cancel-all-btn" onclick="cancelAllOperations()" style="display: none;">
|
||||||
|
Cancel All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-close-section">
|
||||||
|
<button class="download-control-btn secondary" onclick="closeDownloadMissingModal()">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
activeAnalysisTaskId = null;
|
||||||
|
analysisResults = [];
|
||||||
|
missingTracks = [];
|
||||||
|
|
||||||
|
// Show modal
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDownloadMissingModal() {
|
||||||
|
// Clean up any active tasks
|
||||||
|
if (activeAnalysisTaskId) {
|
||||||
|
fetch(`/api/tracks/analyze/cancel/${activeAnalysisTaskId}`, { method: 'POST' })
|
||||||
|
.catch(e => console.warn('Failed to cancel analysis task:', e));
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = document.getElementById('download-missing-modal');
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
activeAnalysisTaskId = null;
|
||||||
|
currentPlaylistTracks = [];
|
||||||
|
analysisResults = [];
|
||||||
|
missingTracks = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startTrackAnalysis() {
|
||||||
|
console.log(`🔍 Starting track analysis for ${currentPlaylistTracks.length} tracks`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update UI to analysis mode
|
||||||
|
document.getElementById('begin-analysis-btn').style.display = 'none';
|
||||||
|
document.getElementById('cancel-all-btn').style.display = 'inline-block';
|
||||||
|
document.getElementById('analysis-progress-text').textContent = 'Starting analysis...';
|
||||||
|
|
||||||
|
// Start analysis
|
||||||
|
const response = await fetch('/api/tracks/analyze', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
tracks: currentPlaylistTracks
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.success) throw new Error(data.error);
|
||||||
|
|
||||||
|
activeAnalysisTaskId = data.task_id;
|
||||||
|
console.log(`✅ Analysis started with task ID: ${activeAnalysisTaskId}`);
|
||||||
|
|
||||||
|
// Start polling for results
|
||||||
|
startAnalysisPolling();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to start analysis:', error);
|
||||||
|
showToast(`Failed to start analysis: ${error.message}`, 'error');
|
||||||
|
|
||||||
|
// Reset UI
|
||||||
|
document.getElementById('begin-analysis-btn').style.display = 'inline-block';
|
||||||
|
document.getElementById('cancel-all-btn').style.display = 'none';
|
||||||
|
document.getElementById('analysis-progress-text').textContent = 'Ready to start';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAnalysisPolling() {
|
||||||
|
if (!activeAnalysisTaskId) return;
|
||||||
|
|
||||||
|
const pollInterval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/tracks/analyze/status/${activeAnalysisTaskId}`);
|
||||||
|
const status = await response.json();
|
||||||
|
|
||||||
|
if (response.status === 404 || status.error) {
|
||||||
|
console.error('❌ Analysis task not found or error:', status.error);
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update progress bar
|
||||||
|
const progressPercent = status.progress || 0;
|
||||||
|
document.getElementById('analysis-progress-fill').style.width = `${progressPercent}%`;
|
||||||
|
document.getElementById('analysis-progress-text').textContent =
|
||||||
|
`${status.processed || 0}/${status.total || 0} tracks analyzed (${progressPercent}%)`;
|
||||||
|
|
||||||
|
// Update table with individual results
|
||||||
|
if (status.results && status.results.length > 0) {
|
||||||
|
updateTrackAnalysisResults(status.results);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if complete
|
||||||
|
if (status.status === 'complete') {
|
||||||
|
console.log(`✅ Analysis complete: ${status.total_found} found, ${status.total_missing} missing`);
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
onAnalysisComplete(status);
|
||||||
|
} else if (status.status === 'error') {
|
||||||
|
console.error('❌ Analysis failed:', status.error);
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
showToast(`Analysis failed: ${status.error}`, 'error');
|
||||||
|
resetToInitialState();
|
||||||
|
} else if (status.status === 'cancelled') {
|
||||||
|
console.log('⚠️ Analysis was cancelled');
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
resetToInitialState();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error polling analysis status:', error);
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
showToast('Failed to get analysis status', 'error');
|
||||||
|
}
|
||||||
|
}, 1000); // Poll every second
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTrackAnalysisResults(results) {
|
||||||
|
for (const result of results) {
|
||||||
|
const trackIndex = result.track_index;
|
||||||
|
const matchElement = document.getElementById(`match-${trackIndex}`);
|
||||||
|
|
||||||
|
if (matchElement) {
|
||||||
|
if (result.found) {
|
||||||
|
matchElement.textContent = '✅ Found';
|
||||||
|
matchElement.className = 'track-match-status match-found';
|
||||||
|
} else {
|
||||||
|
matchElement.textContent = '❌ Missing';
|
||||||
|
matchElement.className = 'track-match-status match-missing';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAnalysisComplete(status) {
|
||||||
|
// Update dashboard stats
|
||||||
|
document.getElementById('stat-found').textContent = status.total_found || 0;
|
||||||
|
document.getElementById('stat-missing').textContent = status.total_missing || 0;
|
||||||
|
|
||||||
|
// Update progress text
|
||||||
|
document.getElementById('analysis-progress-text').textContent = 'Analysis complete!';
|
||||||
|
document.getElementById('download-progress-text').textContent = 'Ready to download missing tracks';
|
||||||
|
|
||||||
|
// Store results
|
||||||
|
analysisResults = status.results || [];
|
||||||
|
missingTracks = analysisResults.filter(r => !r.found);
|
||||||
|
|
||||||
|
console.log(`📊 Analysis results: ${analysisResults.length} total, ${missingTracks.length} missing`);
|
||||||
|
|
||||||
|
// Update UI for download phase
|
||||||
|
document.getElementById('cancel-all-btn').style.display = 'none';
|
||||||
|
if (missingTracks.length > 0) {
|
||||||
|
document.getElementById('start-downloads-btn').style.display = 'inline-block';
|
||||||
|
} else {
|
||||||
|
showToast('All tracks were found in your library!', 'success');
|
||||||
|
document.getElementById('download-progress-text').textContent = 'No downloads needed - all tracks found!';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startMissingDownloads() {
|
||||||
|
if (missingTracks.length === 0) {
|
||||||
|
showToast('No missing tracks to download', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`⏬ Starting downloads for ${missingTracks.length} missing tracks`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update UI
|
||||||
|
document.getElementById('start-downloads-btn').style.display = 'none';
|
||||||
|
document.getElementById('cancel-all-btn').style.display = 'inline-block';
|
||||||
|
document.getElementById('download-progress-text').textContent = 'Queueing downloads...';
|
||||||
|
|
||||||
|
// Add cancel buttons to missing tracks
|
||||||
|
for (const result of missingTracks) {
|
||||||
|
const actionsElement = document.getElementById(`actions-${result.track_index}`);
|
||||||
|
if (actionsElement) {
|
||||||
|
actionsElement.innerHTML = `<button class="cancel-track-btn" onclick="cancelTrackDownload(${result.track_index})">Cancel</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update download status
|
||||||
|
const statusElement = document.getElementById(`download-${result.track_index}`);
|
||||||
|
if (statusElement) {
|
||||||
|
statusElement.textContent = '🔍 Queueing...';
|
||||||
|
statusElement.className = 'track-download-status download-searching';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue downloads
|
||||||
|
const response = await fetch('/api/tracks/download_missing', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
missing_tracks: missingTracks
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.success) throw new Error(data.error);
|
||||||
|
|
||||||
|
console.log(`✅ Queued ${data.queued} downloads`);
|
||||||
|
showToast(`Queued ${data.queued} downloads. Check the download queue for progress.`, 'success');
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
document.getElementById('download-progress-text').textContent =
|
||||||
|
`${data.queued} downloads queued. Check download queue for live progress.`;
|
||||||
|
|
||||||
|
// Update download status for queued tracks
|
||||||
|
for (const result of missingTracks) {
|
||||||
|
const statusElement = document.getElementById(`download-${result.track_index}`);
|
||||||
|
if (statusElement) {
|
||||||
|
statusElement.textContent = '📥 Queued';
|
||||||
|
statusElement.className = 'track-download-status download-searching';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide cancel button since downloads are now handled by the main download system
|
||||||
|
document.getElementById('cancel-all-btn').style.display = 'none';
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to start downloads:', error);
|
||||||
|
showToast(`Failed to start downloads: ${error.message}`, 'error');
|
||||||
|
|
||||||
|
// Reset UI
|
||||||
|
document.getElementById('start-downloads-btn').style.display = 'inline-block';
|
||||||
|
document.getElementById('cancel-all-btn').style.display = 'none';
|
||||||
|
document.getElementById('download-progress-text').textContent = 'Ready to download missing tracks';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelAllOperations() {
|
||||||
|
console.log('🛑 Cancelling all operations');
|
||||||
|
|
||||||
|
// Cancel analysis if running
|
||||||
|
if (activeAnalysisTaskId) {
|
||||||
|
fetch(`/api/tracks/analyze/cancel/${activeAnalysisTaskId}`, { method: 'POST' })
|
||||||
|
.catch(e => console.warn('Failed to cancel analysis:', e));
|
||||||
|
}
|
||||||
|
|
||||||
|
resetToInitialState();
|
||||||
|
showToast('Operations cancelled', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetToInitialState() {
|
||||||
|
// Reset UI
|
||||||
|
document.getElementById('begin-analysis-btn').style.display = 'inline-block';
|
||||||
|
document.getElementById('start-downloads-btn').style.display = 'none';
|
||||||
|
document.getElementById('cancel-all-btn').style.display = 'none';
|
||||||
|
|
||||||
|
// Reset progress bars
|
||||||
|
document.getElementById('analysis-progress-fill').style.width = '0%';
|
||||||
|
document.getElementById('download-progress-fill').style.width = '0%';
|
||||||
|
document.getElementById('analysis-progress-text').textContent = 'Ready to start';
|
||||||
|
document.getElementById('download-progress-text').textContent = 'Waiting for analysis';
|
||||||
|
|
||||||
|
// Reset stats
|
||||||
|
document.getElementById('stat-found').textContent = '-';
|
||||||
|
document.getElementById('stat-missing').textContent = '-';
|
||||||
|
document.getElementById('stat-downloaded').textContent = '0';
|
||||||
|
|
||||||
|
// Reset track table
|
||||||
|
const tbody = document.getElementById('download-tracks-tbody');
|
||||||
|
if (tbody) {
|
||||||
|
const rows = tbody.querySelectorAll('tr');
|
||||||
|
rows.forEach((row, index) => {
|
||||||
|
const matchElement = row.querySelector('.track-match-status');
|
||||||
|
const downloadElement = row.querySelector('.track-download-status');
|
||||||
|
const actionsElement = row.querySelector('.track-actions');
|
||||||
|
|
||||||
|
if (matchElement) {
|
||||||
|
matchElement.textContent = '🔍 Pending';
|
||||||
|
matchElement.className = 'track-match-status match-checking';
|
||||||
|
}
|
||||||
|
if (downloadElement) {
|
||||||
|
downloadElement.textContent = '-';
|
||||||
|
downloadElement.className = 'track-download-status';
|
||||||
|
}
|
||||||
|
if (actionsElement) {
|
||||||
|
actionsElement.textContent = '-';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
activeAnalysisTaskId = null;
|
||||||
|
analysisResults = [];
|
||||||
|
missingTracks = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelTrackDownload(trackIndex) {
|
||||||
|
console.log(`🛑 Cancelling download for track ${trackIndex}`);
|
||||||
|
// Individual track cancellation would need to be implemented in the download system
|
||||||
|
// For now, just update the UI
|
||||||
|
const statusElement = document.getElementById(`download-${trackIndex}`);
|
||||||
|
const actionsElement = document.getElementById(`actions-${trackIndex}`);
|
||||||
|
|
||||||
|
if (statusElement) {
|
||||||
|
statusElement.textContent = '❌ Cancelled';
|
||||||
|
statusElement.className = 'track-download-status download-failed';
|
||||||
|
}
|
||||||
|
if (actionsElement) {
|
||||||
|
actionsElement.textContent = '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Find and REPLACE the old startPlaylistSyncFromModal function
|
// Find and REPLACE the old startPlaylistSyncFromModal function
|
||||||
async function startPlaylistSync(playlistId) {
|
async function startPlaylistSync(playlistId) {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
|
||||||
|
|
@ -4467,6 +4467,18 @@ body {
|
||||||
box-shadow: 0 6px 20px rgba(29, 185, 84, 0.4);
|
box-shadow: 0 6px 20px rgba(29, 185, 84, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.playlist-modal-btn-tertiary {
|
||||||
|
background: #404040;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist-modal-btn-tertiary:hover {
|
||||||
|
background: #505050;
|
||||||
|
border-color: #777777;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
/* Add these styles to the end of style.css */
|
/* Add these styles to the end of style.css */
|
||||||
|
|
||||||
.sync-progress-indicator {
|
.sync-progress-indicator {
|
||||||
|
|
@ -4550,3 +4562,356 @@ body {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==============================================
|
||||||
|
DOWNLOAD MISSING TRACKS MODAL STYLES
|
||||||
|
============================================== */
|
||||||
|
|
||||||
|
#download-missing-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
z-index: 1000;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-missing-modal-content {
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||||
|
width: 1200px;
|
||||||
|
height: 900px;
|
||||||
|
max-width: 90vw;
|
||||||
|
max-height: 90vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-missing-modal-header {
|
||||||
|
background: linear-gradient(135deg, #2d2d2d 0%, #1a1a1a 100%);
|
||||||
|
border-bottom: 1px solid #404040;
|
||||||
|
padding: 20px 25px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-missing-modal-title {
|
||||||
|
color: #1db954;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-missing-modal-close {
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 300;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
line-height: 1;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-missing-modal-close:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-missing-modal-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 25px;
|
||||||
|
gap: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Stats Section */
|
||||||
|
.download-dashboard-stats {
|
||||||
|
background: #2d2d2d;
|
||||||
|
border: 1px solid #444444;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-stat {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-stat-number {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-stat-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #cccccc;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-total .dashboard-stat-number { color: #1db954; }
|
||||||
|
.stat-found .dashboard-stat-number { color: #4CAF50; }
|
||||||
|
.stat-missing .dashboard-stat-number { color: #FF6B35; }
|
||||||
|
.stat-downloaded .dashboard-stat-number { color: #2196F3; }
|
||||||
|
|
||||||
|
/* Progress Section */
|
||||||
|
.download-progress-section {
|
||||||
|
background: #2d2d2d;
|
||||||
|
border: 1px solid #444444;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #cccccc;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
background: #404040;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
background: linear-gradient(90deg, #1db954, #1ed760);
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
width: 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill.analysis { background: linear-gradient(90deg, #2196F3, #21CBF3); }
|
||||||
|
.progress-fill.download { background: linear-gradient(90deg, #FF6B35, #FF8A35); }
|
||||||
|
|
||||||
|
/* Track Table Section */
|
||||||
|
.download-tracks-section {
|
||||||
|
background: #2d2d2d;
|
||||||
|
border: 1px solid #444444;
|
||||||
|
border-radius: 12px;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-tracks-header {
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-bottom: 1px solid #444444;
|
||||||
|
background: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-tracks-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-tracks-table-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-tracks-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-tracks-table th {
|
||||||
|
background: #404040;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: left;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-bottom: 1px solid #555555;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-tracks-table td {
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-bottom: 1px solid #333333;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-tracks-table tr:hover {
|
||||||
|
background: rgba(29, 185, 84, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-number {
|
||||||
|
color: #888888;
|
||||||
|
font-weight: 500;
|
||||||
|
width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-artist {
|
||||||
|
color: #cccccc;
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-duration {
|
||||||
|
color: #999999;
|
||||||
|
text-align: center;
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-match-status {
|
||||||
|
text-align: center;
|
||||||
|
width: 100px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-found { color: #4CAF50; }
|
||||||
|
.match-missing { color: #FF6B35; }
|
||||||
|
.match-checking { color: #2196F3; }
|
||||||
|
|
||||||
|
.track-download-status {
|
||||||
|
text-align: center;
|
||||||
|
width: 120px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-searching { color: #2196F3; }
|
||||||
|
.download-downloading { color: #FF6B35; }
|
||||||
|
.download-complete { color: #4CAF50; }
|
||||||
|
.download-failed { color: #f44336; }
|
||||||
|
|
||||||
|
.track-actions {
|
||||||
|
text-align: center;
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-track-btn {
|
||||||
|
background: #f44336;
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-track-btn:hover {
|
||||||
|
background: #d32f2f;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Footer */
|
||||||
|
.download-missing-modal-footer {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-top: 1px solid #404040;
|
||||||
|
padding: 20px 25px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-phase-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-control-btn {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-control-btn.primary {
|
||||||
|
background: linear-gradient(135deg, #1db954, #1ed760);
|
||||||
|
color: #000000;
|
||||||
|
box-shadow: 0 4px 16px rgba(29, 185, 84, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-control-btn.primary:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 6px 20px rgba(29, 185, 84, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-control-btn.secondary {
|
||||||
|
background: #404040;
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-control-btn.secondary:hover {
|
||||||
|
background: #505050;
|
||||||
|
border-color: #777777;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-control-btn.danger {
|
||||||
|
background: #f44336;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-control-btn.danger:hover {
|
||||||
|
background: #d32f2f;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-control-btn:disabled {
|
||||||
|
background: #333333;
|
||||||
|
color: #666666;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue