detect lower quality files for watchlist / all artists.
This commit is contained in:
parent
5688f7a89d
commit
37c9de0007
3 changed files with 580 additions and 1 deletions
400
web_server.py
400
web_server.py
|
|
@ -155,6 +155,22 @@ db_update_state = {
|
|||
"error_message": ""
|
||||
}
|
||||
|
||||
# Quality Scanner state
|
||||
quality_scanner_state = {
|
||||
"status": "idle", # idle, running, finished, error
|
||||
"phase": "Ready to scan",
|
||||
"progress": 0,
|
||||
"processed": 0,
|
||||
"total": 0,
|
||||
"quality_met": 0,
|
||||
"low_quality": 0,
|
||||
"matched": 0,
|
||||
"error_message": "",
|
||||
"results": [] # List of low quality tracks with match status
|
||||
}
|
||||
quality_scanner_lock = threading.Lock()
|
||||
quality_scanner_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="QualityScanner")
|
||||
|
||||
# --- Sync Page Globals ---
|
||||
sync_executor = ThreadPoolExecutor(max_workers=3, thread_name_prefix="SyncWorker")
|
||||
active_sync_workers = {} # Key: playlist_id, Value: Future object
|
||||
|
|
@ -8037,6 +8053,390 @@ def stop_database_update():
|
|||
else:
|
||||
return jsonify({"success": False, "error": "No update is currently running."}), 404
|
||||
|
||||
# ===============================
|
||||
# == QUALITY SCANNER ==
|
||||
# ===============================
|
||||
|
||||
# Quality tier mappings
|
||||
QUALITY_TIERS = {
|
||||
'lossless': {
|
||||
'extensions': ['.flac', '.ape', '.wav', '.alac', '.dsf', '.dff', '.aiff', '.aif'],
|
||||
'tier': 1
|
||||
},
|
||||
'high_lossy': {
|
||||
'extensions': ['.opus', '.ogg'],
|
||||
'tier': 2
|
||||
},
|
||||
'standard_lossy': {
|
||||
'extensions': ['.m4a', '.aac'],
|
||||
'tier': 3
|
||||
},
|
||||
'low_lossy': {
|
||||
'extensions': ['.mp3', '.wma'],
|
||||
'tier': 4
|
||||
}
|
||||
}
|
||||
|
||||
def _get_quality_tier_from_extension(file_path):
|
||||
"""Determine quality tier from file extension"""
|
||||
if not file_path:
|
||||
return ('unknown', 999)
|
||||
|
||||
ext = os.path.splitext(file_path)[1].lower()
|
||||
|
||||
for tier_name, tier_data in QUALITY_TIERS.items():
|
||||
if ext in tier_data['extensions']:
|
||||
return (tier_name, tier_data['tier'])
|
||||
|
||||
return ('unknown', 999)
|
||||
|
||||
def _run_quality_scanner(scope='watchlist'):
|
||||
"""Main quality scanner worker function"""
|
||||
from core.wishlist_service import get_wishlist_service
|
||||
from database.music_database import MusicDatabase
|
||||
|
||||
try:
|
||||
with quality_scanner_lock:
|
||||
quality_scanner_state["status"] = "running"
|
||||
quality_scanner_state["phase"] = "Initializing scan..."
|
||||
quality_scanner_state["progress"] = 0
|
||||
quality_scanner_state["processed"] = 0
|
||||
quality_scanner_state["total"] = 0
|
||||
quality_scanner_state["quality_met"] = 0
|
||||
quality_scanner_state["low_quality"] = 0
|
||||
quality_scanner_state["matched"] = 0
|
||||
quality_scanner_state["results"] = []
|
||||
quality_scanner_state["error_message"] = ""
|
||||
|
||||
print(f"🔍 [Quality Scanner] Starting scan with scope: {scope}")
|
||||
|
||||
# Get database instance
|
||||
db = MusicDatabase()
|
||||
|
||||
# Get quality profile to determine preferred quality
|
||||
quality_profile = db.get_quality_profile()
|
||||
preferred_qualities = quality_profile.get('qualities', {})
|
||||
|
||||
# Determine minimum acceptable tier based on enabled qualities
|
||||
min_acceptable_tier = 999
|
||||
for quality_name, quality_config in preferred_qualities.items():
|
||||
if quality_config.get('enabled', False):
|
||||
# Map quality profile names to tier names
|
||||
tier_map = {
|
||||
'flac': 'lossless',
|
||||
'mp3_320': 'low_lossy',
|
||||
'mp3_256': 'low_lossy',
|
||||
'mp3_192': 'low_lossy'
|
||||
}
|
||||
tier_name = tier_map.get(quality_name)
|
||||
if tier_name:
|
||||
tier_num = QUALITY_TIERS[tier_name]['tier']
|
||||
min_acceptable_tier = min(min_acceptable_tier, tier_num)
|
||||
|
||||
print(f"🎵 [Quality Scanner] Minimum acceptable tier: {min_acceptable_tier}")
|
||||
|
||||
# Get tracks to scan based on scope
|
||||
with quality_scanner_lock:
|
||||
quality_scanner_state["phase"] = "Loading tracks from database..."
|
||||
|
||||
if scope == 'watchlist':
|
||||
# Get watchlist artists
|
||||
watchlist_artists = db.get_watchlist_artists()
|
||||
if not watchlist_artists:
|
||||
with quality_scanner_lock:
|
||||
quality_scanner_state["status"] = "finished"
|
||||
quality_scanner_state["phase"] = "No watchlist artists found"
|
||||
quality_scanner_state["error_message"] = "Please add artists to watchlist first"
|
||||
print(f"⚠️ [Quality Scanner] No watchlist artists found")
|
||||
return
|
||||
|
||||
# Get artist names from watchlist
|
||||
artist_names = [artist.artist_name for artist in watchlist_artists]
|
||||
print(f"📋 [Quality Scanner] Scanning {len(artist_names)} watchlist artists")
|
||||
|
||||
# Get all tracks for these artists by name
|
||||
conn = db._get_connection()
|
||||
placeholders = ','.join(['?' for _ in artist_names])
|
||||
tracks_to_scan = conn.execute(
|
||||
f"SELECT t.id, t.title, t.artist_id, t.album_id, t.file_path, t.bitrate, a.name as artist_name, al.title as album_title "
|
||||
f"FROM tracks t "
|
||||
f"JOIN artists a ON t.artist_id = a.id "
|
||||
f"JOIN albums al ON t.album_id = al.id "
|
||||
f"WHERE a.name IN ({placeholders}) AND t.file_path IS NOT NULL",
|
||||
artist_names
|
||||
).fetchall()
|
||||
conn.close()
|
||||
else:
|
||||
# Scan all library tracks
|
||||
with quality_scanner_lock:
|
||||
quality_scanner_state["phase"] = "Loading all library tracks..."
|
||||
|
||||
conn = db._get_connection()
|
||||
tracks_to_scan = conn.execute(
|
||||
"SELECT t.id, t.title, t.artist_id, t.album_id, t.file_path, t.bitrate, a.name as artist_name, al.title as album_title "
|
||||
"FROM tracks t "
|
||||
"JOIN artists a ON t.artist_id = a.id "
|
||||
"JOIN albums al ON t.album_id = al.id "
|
||||
"WHERE t.file_path IS NOT NULL"
|
||||
).fetchall()
|
||||
conn.close()
|
||||
|
||||
total_tracks = len(tracks_to_scan)
|
||||
print(f"📊 [Quality Scanner] Found {total_tracks} tracks to scan")
|
||||
|
||||
with quality_scanner_lock:
|
||||
quality_scanner_state["total"] = total_tracks
|
||||
quality_scanner_state["phase"] = f"Scanning {total_tracks} tracks..."
|
||||
|
||||
# Initialize Spotify client for matching
|
||||
spotify_client = SpotifyClient()
|
||||
if not spotify_client.is_authenticated():
|
||||
with quality_scanner_lock:
|
||||
quality_scanner_state["status"] = "error"
|
||||
quality_scanner_state["phase"] = "Spotify not authenticated"
|
||||
quality_scanner_state["error_message"] = "Please authenticate with Spotify first"
|
||||
print(f"❌ [Quality Scanner] Spotify not authenticated")
|
||||
return
|
||||
|
||||
wishlist_service = get_wishlist_service()
|
||||
|
||||
# Scan each track
|
||||
for idx, track_row in enumerate(tracks_to_scan, 1):
|
||||
try:
|
||||
track_id, title, artist_id, album_id, file_path, bitrate, artist_name, album_title = track_row
|
||||
|
||||
# Check quality tier
|
||||
tier_name, tier_num = _get_quality_tier_from_extension(file_path)
|
||||
|
||||
# Update progress
|
||||
with quality_scanner_lock:
|
||||
quality_scanner_state["processed"] = idx
|
||||
quality_scanner_state["progress"] = (idx / total_tracks) * 100
|
||||
quality_scanner_state["phase"] = f"Scanning: {artist_name} - {title}"
|
||||
|
||||
# Check if meets quality standards
|
||||
if tier_num <= min_acceptable_tier:
|
||||
# Quality met
|
||||
with quality_scanner_lock:
|
||||
quality_scanner_state["quality_met"] += 1
|
||||
continue
|
||||
|
||||
# Low quality track found
|
||||
with quality_scanner_lock:
|
||||
quality_scanner_state["low_quality"] += 1
|
||||
|
||||
print(f"🔍 [Quality Scanner] Low quality: {artist_name} - {title} ({tier_name}, {file_path})")
|
||||
|
||||
# Attempt to match to Spotify using matching_engine
|
||||
matched = False
|
||||
matched_track_data = None
|
||||
|
||||
try:
|
||||
# Generate search queries using matching engine
|
||||
temp_track = type('TempTrack', (), {
|
||||
'name': title,
|
||||
'artists': [artist_name],
|
||||
'album': album_title
|
||||
})()
|
||||
|
||||
search_queries = matching_engine.generate_download_queries(temp_track)
|
||||
print(f"🔍 [Quality Scanner] Generated {len(search_queries)} search queries for {artist_name} - {title}")
|
||||
|
||||
# Find best match using confidence scoring
|
||||
best_match = None
|
||||
best_confidence = 0.0
|
||||
min_confidence = 0.7 # Match existing standard
|
||||
|
||||
for query_idx, search_query in enumerate(search_queries):
|
||||
try:
|
||||
spotify_matches = spotify_client.search_tracks(search_query, limit=5)
|
||||
|
||||
if not spotify_matches:
|
||||
continue
|
||||
|
||||
# Score each result using matching engine
|
||||
for spotify_track in spotify_matches:
|
||||
try:
|
||||
# Calculate artist confidence
|
||||
artist_confidence = 0.0
|
||||
if spotify_track.artists:
|
||||
for result_artist in spotify_track.artists:
|
||||
artist_sim = matching_engine.similarity_score(
|
||||
matching_engine.normalize_string(artist_name),
|
||||
matching_engine.normalize_string(result_artist)
|
||||
)
|
||||
artist_confidence = max(artist_confidence, artist_sim)
|
||||
|
||||
# Calculate title confidence
|
||||
title_confidence = matching_engine.similarity_score(
|
||||
matching_engine.normalize_string(title),
|
||||
matching_engine.normalize_string(spotify_track.name)
|
||||
)
|
||||
|
||||
# Combined confidence (50% artist + 50% title)
|
||||
combined_confidence = (artist_confidence * 0.5 + title_confidence * 0.5)
|
||||
|
||||
print(f"🔍 [Quality Scanner] Candidate: '{spotify_track.artists[0]}' - '{spotify_track.name}' (confidence: {combined_confidence:.3f})")
|
||||
|
||||
# Update best match if this is better
|
||||
if combined_confidence > best_confidence and combined_confidence >= min_confidence:
|
||||
best_confidence = combined_confidence
|
||||
best_match = spotify_track
|
||||
print(f"✅ [Quality Scanner] New best match: {spotify_track.artists[0]} - {spotify_track.name} (confidence: {combined_confidence:.3f})")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ [Quality Scanner] Error scoring result: {e}")
|
||||
continue
|
||||
|
||||
# If we found a very high confidence match, stop searching
|
||||
if best_confidence >= 0.9:
|
||||
print(f"🎯 [Quality Scanner] High confidence match found ({best_confidence:.3f}), stopping search")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ [Quality Scanner] Error searching with query '{search_query}': {e}")
|
||||
continue
|
||||
|
||||
# Process best match
|
||||
if best_match:
|
||||
matched = True
|
||||
print(f"✅ [Quality Scanner] Final match: {best_match.artists[0]} - {best_match.name} (confidence: {best_confidence:.3f})")
|
||||
|
||||
# Build full Spotify track data for wishlist
|
||||
matched_track_data = {
|
||||
'id': best_match.id,
|
||||
'name': best_match.name,
|
||||
'artists': [{'name': artist} for artist in best_match.artists],
|
||||
'album': {'name': best_match.album},
|
||||
'duration_ms': best_match.duration_ms,
|
||||
'popularity': best_match.popularity,
|
||||
'preview_url': best_match.preview_url,
|
||||
'external_urls': best_match.external_urls or {}
|
||||
}
|
||||
|
||||
# Add to wishlist
|
||||
source_context = {
|
||||
'quality_scanner': True,
|
||||
'original_file_path': file_path,
|
||||
'original_format': tier_name,
|
||||
'original_bitrate': bitrate,
|
||||
'match_confidence': best_confidence,
|
||||
'scan_date': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
success = wishlist_service.add_spotify_track_to_wishlist(
|
||||
spotify_track_data=matched_track_data,
|
||||
failure_reason=f"Low quality - {tier_name.replace('_', ' ').title()} format",
|
||||
source_type='quality_scanner',
|
||||
source_context=source_context
|
||||
)
|
||||
|
||||
if success:
|
||||
with quality_scanner_lock:
|
||||
quality_scanner_state["matched"] += 1
|
||||
print(f"✅ [Quality Scanner] Matched and added to wishlist: {artist_name} - {title}")
|
||||
else:
|
||||
print(f"⚠️ [Quality Scanner] Failed to add to wishlist: {artist_name} - {title}")
|
||||
else:
|
||||
print(f"⚠️ [Quality Scanner] No suitable match found (best confidence: {best_confidence:.3f}, required: {min_confidence:.3f})")
|
||||
|
||||
except Exception as matching_error:
|
||||
print(f"❌ [Quality Scanner] Matching error for {artist_name} - {title}: {matching_error}")
|
||||
|
||||
# Store result
|
||||
result_entry = {
|
||||
'track_id': track_id,
|
||||
'title': title,
|
||||
'artist': artist_name,
|
||||
'album': album_title,
|
||||
'file_path': file_path,
|
||||
'current_format': tier_name,
|
||||
'bitrate': bitrate,
|
||||
'matched': matched,
|
||||
'spotify_id': matched_track_data['id'] if matched_track_data else None
|
||||
}
|
||||
|
||||
with quality_scanner_lock:
|
||||
quality_scanner_state["results"].append(result_entry)
|
||||
|
||||
if not matched:
|
||||
print(f"⚠️ [Quality Scanner] No Spotify match found for: {artist_name} - {title}")
|
||||
|
||||
except Exception as track_error:
|
||||
print(f"❌ [Quality Scanner] Error processing track: {track_error}")
|
||||
continue
|
||||
|
||||
# Scan complete
|
||||
with quality_scanner_lock:
|
||||
quality_scanner_state["status"] = "finished"
|
||||
quality_scanner_state["progress"] = 100
|
||||
quality_scanner_state["phase"] = "Scan complete"
|
||||
|
||||
print(f"✅ [Quality Scanner] Scan complete: {quality_scanner_state['processed']} processed, "
|
||||
f"{quality_scanner_state['low_quality']} low quality, {quality_scanner_state['matched']} matched to Spotify")
|
||||
|
||||
# Add activity
|
||||
add_activity_item("🔍", "Quality Scan Complete",
|
||||
f"{quality_scanner_state['matched']} tracks added to wishlist", "Now")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ [Quality Scanner] Critical error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
with quality_scanner_lock:
|
||||
quality_scanner_state["status"] = "error"
|
||||
quality_scanner_state["error_message"] = str(e)
|
||||
quality_scanner_state["phase"] = f"Error: {str(e)}"
|
||||
|
||||
@app.route('/api/quality-scanner/start', methods=['POST'])
|
||||
def start_quality_scan():
|
||||
"""Start the quality scanner"""
|
||||
with quality_scanner_lock:
|
||||
if quality_scanner_state["status"] == "running":
|
||||
return jsonify({"success": False, "error": "A scan is already in progress"}), 409
|
||||
|
||||
data = request.get_json() or {}
|
||||
scope = data.get('scope', 'watchlist') # 'watchlist' or 'all'
|
||||
|
||||
print(f"🔍 [Quality Scanner API] Starting scan with scope: {scope}")
|
||||
|
||||
# Reset state
|
||||
quality_scanner_state["status"] = "running"
|
||||
quality_scanner_state["phase"] = "Initializing..."
|
||||
quality_scanner_state["progress"] = 0
|
||||
quality_scanner_state["processed"] = 0
|
||||
quality_scanner_state["total"] = 0
|
||||
quality_scanner_state["quality_met"] = 0
|
||||
quality_scanner_state["low_quality"] = 0
|
||||
quality_scanner_state["matched"] = 0
|
||||
quality_scanner_state["results"] = []
|
||||
quality_scanner_state["error_message"] = ""
|
||||
|
||||
# Submit worker
|
||||
quality_scanner_executor.submit(_run_quality_scanner, scope)
|
||||
|
||||
add_activity_item("🔍", "Quality Scan Started", f"Scanning {scope} tracks", "Now")
|
||||
|
||||
return jsonify({"success": True, "message": "Quality scan started"})
|
||||
|
||||
@app.route('/api/quality-scanner/status', methods=['GET'])
|
||||
def get_quality_scanner_status():
|
||||
"""Get current quality scanner status"""
|
||||
with quality_scanner_lock:
|
||||
return jsonify(quality_scanner_state)
|
||||
|
||||
@app.route('/api/quality-scanner/stop', methods=['POST'])
|
||||
def stop_quality_scan():
|
||||
"""Stop the quality scanner (sets a stop flag)"""
|
||||
with quality_scanner_lock:
|
||||
if quality_scanner_state["status"] == "running":
|
||||
quality_scanner_state["status"] = "finished"
|
||||
quality_scanner_state["phase"] = "Scan stopped by user"
|
||||
return jsonify({"success": True, "message": "Stop request sent"})
|
||||
else:
|
||||
return jsonify({"success": False, "error": "No scan is currently running"}), 404
|
||||
|
||||
# ===============================
|
||||
# == DOWNLOAD MISSING TRACKS ==
|
||||
# ===============================
|
||||
|
|
|
|||
|
|
@ -300,6 +300,43 @@
|
|||
<p class="progress-details-label" id="metadata-progress-label">0 / 0 artists (0.0%)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tool-card" id="quality-scanner-card">
|
||||
<h4 class="tool-card-title">Quality Scanner</h4>
|
||||
<p class="tool-card-info">Scan library for tracks below quality preferences</p>
|
||||
<div class="tool-card-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-item-label">Processed:</span>
|
||||
<span class="stat-item-value" id="quality-stat-processed">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-item-label">Quality Met:</span>
|
||||
<span class="stat-item-value" id="quality-stat-met">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-item-label">Low Quality:</span>
|
||||
<span class="stat-item-value" id="quality-stat-low">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-item-label">Matched:</span>
|
||||
<span class="stat-item-value" id="quality-stat-matched">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tool-card-controls">
|
||||
<select id="quality-scan-scope">
|
||||
<option value="watchlist">Watchlist Artists Only</option>
|
||||
<option value="all">All Library Tracks</option>
|
||||
</select>
|
||||
<button id="quality-scan-button">Scan Library</button>
|
||||
</div>
|
||||
<div class="tool-card-progress-section">
|
||||
<p class="progress-phase-label" id="quality-phase-label">Ready to scan</p>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar-fill" id="quality-progress-bar" style="width: 0%;"></div>
|
||||
</div>
|
||||
<p class="progress-details-label" id="quality-progress-label">0 / 0 tracks scanned (0.0%)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ let isSortReversed = false;
|
|||
let searchAbortController = null;
|
||||
let dbStatsInterval = null;
|
||||
let dbUpdateStatusInterval = null;
|
||||
let qualityScannerStatusInterval = null;
|
||||
let wishlistCountInterval = null;
|
||||
|
||||
// --- Add these globals for the Sync Page ---
|
||||
|
|
@ -8781,6 +8782,138 @@ function stopDbUpdatePolling() {
|
|||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// QUALITY SCANNER TOOL
|
||||
// ===================================================================
|
||||
|
||||
async function handleQualityScanButtonClick() {
|
||||
const button = document.getElementById('quality-scan-button');
|
||||
const currentAction = button.textContent;
|
||||
|
||||
if (currentAction === 'Scan Library') {
|
||||
const scopeSelect = document.getElementById('quality-scan-scope');
|
||||
const scope = scopeSelect.value;
|
||||
|
||||
try {
|
||||
button.disabled = true;
|
||||
button.textContent = 'Starting...';
|
||||
const response = await fetch('/api/quality-scanner/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ scope: scope })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showToast('Quality scan started!', 'success');
|
||||
// Start polling immediately to get live status
|
||||
checkAndUpdateQualityScanProgress();
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
showToast(`Error: ${errorData.error}`, 'error');
|
||||
button.disabled = false;
|
||||
button.textContent = 'Scan Library';
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Failed to start quality scan.', 'error');
|
||||
button.disabled = false;
|
||||
button.textContent = 'Scan Library';
|
||||
}
|
||||
|
||||
} else { // "Stop Scan"
|
||||
try {
|
||||
const response = await fetch('/api/quality-scanner/stop', { method: 'POST' });
|
||||
if (response.ok) {
|
||||
showToast('Stop request sent.', 'info');
|
||||
} else {
|
||||
showToast('Failed to send stop request.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Error sending stop request.', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkAndUpdateQualityScanProgress() {
|
||||
try {
|
||||
const response = await fetch('/api/quality-scanner/status', {
|
||||
signal: AbortSignal.timeout(10000) // 10 second timeout
|
||||
});
|
||||
if (!response.ok) return;
|
||||
|
||||
const state = await response.json();
|
||||
console.debug('🔍 Quality Scanner Status:', state.status, `${state.processed}/${state.total}`, `${state.progress.toFixed(1)}%`);
|
||||
updateQualityScanProgressUI(state);
|
||||
|
||||
// Start polling only if not already polling and status is running
|
||||
if (state.status === 'running' && !qualityScannerStatusInterval) {
|
||||
console.log('🔄 Starting quality scanner polling (1 second interval)');
|
||||
qualityScannerStatusInterval = setInterval(checkAndUpdateQualityScanProgress, 1000);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Could not fetch quality scanner status:', error);
|
||||
// Don't stop polling on network errors - keep trying
|
||||
}
|
||||
}
|
||||
|
||||
function updateQualityScanProgressUI(state) {
|
||||
const button = document.getElementById('quality-scan-button');
|
||||
const phaseLabel = document.getElementById('quality-phase-label');
|
||||
const progressLabel = document.getElementById('quality-progress-label');
|
||||
const progressBar = document.getElementById('quality-progress-bar');
|
||||
const scopeSelect = document.getElementById('quality-scan-scope');
|
||||
|
||||
// Stats
|
||||
const processedStat = document.getElementById('quality-stat-processed');
|
||||
const metStat = document.getElementById('quality-stat-met');
|
||||
const lowStat = document.getElementById('quality-stat-low');
|
||||
const matchedStat = document.getElementById('quality-stat-matched');
|
||||
|
||||
if (!button || !phaseLabel || !progressLabel || !progressBar || !scopeSelect) return;
|
||||
|
||||
// Update stats
|
||||
if (processedStat) processedStat.textContent = state.processed || 0;
|
||||
if (metStat) metStat.textContent = state.quality_met || 0;
|
||||
if (lowStat) lowStat.textContent = state.low_quality || 0;
|
||||
if (matchedStat) matchedStat.textContent = state.matched || 0;
|
||||
|
||||
if (state.status === 'running') {
|
||||
button.textContent = 'Stop Scan';
|
||||
button.disabled = false;
|
||||
scopeSelect.disabled = true;
|
||||
|
||||
phaseLabel.textContent = state.phase || 'Scanning...';
|
||||
progressLabel.textContent = `${state.processed} / ${state.total} tracks scanned (${state.progress.toFixed(1)}%)`;
|
||||
progressBar.style.width = `${state.progress}%`;
|
||||
} else { // idle, finished, or error
|
||||
stopQualityScannerPolling();
|
||||
button.textContent = 'Scan Library';
|
||||
button.disabled = false;
|
||||
scopeSelect.disabled = false;
|
||||
|
||||
if (state.status === 'error') {
|
||||
phaseLabel.textContent = `Error: ${state.error_message}`;
|
||||
progressBar.style.backgroundColor = '#ff4444'; // Red for error
|
||||
} else {
|
||||
phaseLabel.textContent = state.phase || 'Ready to scan';
|
||||
progressBar.style.backgroundColor = '#1db954'; // Green for normal
|
||||
}
|
||||
|
||||
if (state.status === 'finished') {
|
||||
// Show completion toast with results
|
||||
showToast(`Scan complete! ${state.matched} tracks added to wishlist`, 'success');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function stopQualityScannerPolling() {
|
||||
if (qualityScannerStatusInterval) {
|
||||
console.log('⏹️ Stopping quality scanner polling');
|
||||
clearInterval(qualityScannerStatusInterval);
|
||||
qualityScannerStatusInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function stopWishlistCountPolling() {
|
||||
if (wishlistCountInterval) {
|
||||
clearInterval(wishlistCountInterval);
|
||||
|
|
@ -8880,6 +9013,12 @@ async function loadDashboardData() {
|
|||
// Check for ongoing metadata update and restore state
|
||||
await checkAndRestoreMetadataUpdateState();
|
||||
|
||||
// Attach event listener for the quality scanner tool
|
||||
const qualityScanButton = document.getElementById('quality-scan-button');
|
||||
if (qualityScanButton) {
|
||||
qualityScanButton.addEventListener('click', handleQualityScanButtonClick);
|
||||
}
|
||||
|
||||
// Attach event listener for the wishlist button
|
||||
const wishlistButton = document.getElementById('wishlist-button');
|
||||
if (wishlistButton) {
|
||||
|
|
@ -8919,7 +9058,10 @@ async function loadDashboardData() {
|
|||
|
||||
// Also check the status of any ongoing update when the page loads
|
||||
await checkAndUpdateDbProgress();
|
||||
|
||||
|
||||
// Check for any ongoing quality scanner when the page loads
|
||||
await checkAndUpdateQualityScanProgress();
|
||||
|
||||
// Check for any active download processes that need rehydration
|
||||
await checkForActiveProcesses();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue