download missing tracks modal

pull/15/head
Broque Thomas 9 months ago
parent 584245f8a4
commit 632f45c68e

@ -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()">&times;</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…
Cancel
Save