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:
|
||||
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 ==
|
||||
# ===============================
|
||||
|
|
|
|||
|
|
@ -1578,6 +1578,7 @@ function showPlaylistDetailsModal(playlist) {
|
|||
|
||||
<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-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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1599,6 +1600,466 @@ function formatDuration(ms) {
|
|||
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
|
||||
async function startPlaylistSync(playlistId) {
|
||||
const startTime = Date.now();
|
||||
|
|
|
|||
|
|
@ -4467,6 +4467,18 @@ body {
|
|||
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 */
|
||||
|
||||
.sync-progress-indicator {
|
||||
|
|
@ -4549,4 +4561,357 @@ body {
|
|||
gap: 6px;
|
||||
font-size: 11px;
|
||||
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