From 8ba20f7c499ae656c7f56c86a4d18e861692c5ff Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Wed, 3 Jun 2026 15:26:29 -0700 Subject: [PATCH] Similar Artists worker: DB-backed stats (orb matches modal) + default on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - get_stats now reports PERSISTENT counts from the DB (matched/not_found/pending + a progress.artists breakdown) instead of in-memory session counters, so the dashboard orb tooltip and the Manage modal agree (was showing 0 vs 14 after a restart) and it survives restarts — same approach as the other workers. - Orb tooltip reads progress.artists ('Artists: 14 / 15 (93%)') like the rest. - Worker now defaults to ON (running) instead of opt-in-paused; still honors a saved pause across restarts. It self-paces (~3s/artist) and backs off on MusicMap outages, so the orb spins/active like the others when there's work. 10 seam tests pass. --- core/similar_artists_worker.py | 52 +++++++++++++++++++++++++--------- web_server.py | 10 +++---- webui/static/enrichment.js | 8 +++--- 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/core/similar_artists_worker.py b/core/similar_artists_worker.py index 93db6416..c2bca2e5 100644 --- a/core/similar_artists_worker.py +++ b/core/similar_artists_worker.py @@ -167,12 +167,17 @@ class SimilarArtistsWorker: self.paused = False def get_stats(self) -> Dict[str, Any]: - try: - self.stats['pending'] = self._count_pending() - except Exception: - pass + # Report PERSISTENT counts from the DB (not the in-memory session + # counters), so the dashboard orb and the Manage modal always agree and + # survive restarts — same approach as the other enrichment workers. + c = self._db_counts() + self.stats = { + 'matched': c['matched'], 'not_found': c['not_found'], + 'pending': c['pending'], 'errors': c['error'], + } + total = c['total'] is_running = self.running and (self.thread is not None and self.thread.is_alive()) - is_idle = is_running and not self.paused and self.stats['pending'] == 0 and self.current_item is None + is_idle = is_running and not self.paused and c['pending'] == 0 and self.current_item is None return { 'enabled': True, 'running': is_running and not self.paused, @@ -180,7 +185,14 @@ class SimilarArtistsWorker: 'idle': is_idle, 'current_item': self.current_item, 'stats': self.stats.copy(), - 'progress': {}, + # Artist-only progress (no album/track phases) so the orb tooltip can + # show "matched / total (percent%)" like every other worker. + 'progress': { + 'artists': { + 'matched': c['matched'], 'total': total, + 'percent': int(round(c['matched'] / total * 100)) if total else 0, + } + }, } # ── worker loop ──────────────────────────────────────────────────────── @@ -284,13 +296,27 @@ class SimilarArtistsWorker: logger.debug("Similar Artists _mark failed for %s: %s", artist_id, exc) def _count_pending(self) -> int: + return self._db_counts()['pending'] + + def _db_counts(self) -> Dict[str, int]: + """Persistent tallies over the worker's universe (source-matched library + artists): matched / not_found / error / pending(NULL) / total.""" + out = {'matched': 0, 'not_found': 0, 'error': 0, 'pending': 0, 'total': 0} try: conn = self.db._get_connection() cursor = conn.cursor() - cursor.execute( - f"SELECT COUNT(*) FROM artists WHERE similar_artists_match_status IS NULL " - f"AND {self._has_source_id_clause()}" - ) - return int(cursor.fetchone()[0] or 0) - except Exception: - return 0 + cursor.execute(f""" + SELECT + SUM(CASE WHEN similar_artists_match_status = 'matched' THEN 1 ELSE 0 END), + SUM(CASE WHEN similar_artists_match_status = 'not_found' THEN 1 ELSE 0 END), + SUM(CASE WHEN similar_artists_match_status = 'error' THEN 1 ELSE 0 END), + SUM(CASE WHEN similar_artists_match_status IS NULL THEN 1 ELSE 0 END), + COUNT(*) + FROM artists WHERE {self._has_source_id_clause()} + """) + row = cursor.fetchone() or (0, 0, 0, 0, 0) + out.update(matched=int(row[0] or 0), not_found=int(row[1] or 0), + error=int(row[2] or 0), pending=int(row[3] or 0), total=int(row[4] or 0)) + except Exception as exc: + logger.debug("Similar Artists _db_counts failed: %s", exc) + return out diff --git a/web_server.py b/web_server.py index 7136fbb9..adbc8bb6 100644 --- a/web_server.py +++ b/web_server.py @@ -32891,18 +32891,18 @@ except Exception as e: # --- Similar Artists Worker Initialization --- # Fills the similar_artists table for LIBRARY artists (the watchlist scanner only -# covers watchlist artists). Opt-in by default like Amazon: it leans on MusicMap -# (a scraped, outage-prone source) and does per-candidate metadata-source -# searches across the whole library, so it stays paused until the user enables it. +# covers watchlist artists). Runs by default (like the metadata workers); it +# self-paces (~3s/artist) and backs off on MusicMap outages. Respects a saved +# pause choice across restarts. similar_artists_worker = None try: from core.similar_artists_worker import SimilarArtistsWorker similar_artists_db = MusicDatabase() similar_artists_worker = SimilarArtistsWorker(database=similar_artists_db) similar_artists_worker.start() - if config_manager.get('similar_artists_enrichment_paused', True): + if config_manager.get('similar_artists_enrichment_paused', False): similar_artists_worker.pause() - logger.info("Similar Artists worker initialized (paused — enable it to populate library similars)") + logger.info("Similar Artists worker initialized (paused — restored from config)") else: logger.info("Similar Artists worker initialized and started") except Exception as e: diff --git a/webui/static/enrichment.js b/webui/static/enrichment.js index efed15f4..8fba45d9 100644 --- a/webui/static/enrichment.js +++ b/webui/static/enrichment.js @@ -1351,10 +1351,10 @@ function updateSimilarArtistsEnrichmentStatusFromData(data) { else if (data.current_item) tCurrent.textContent = `Now: ${data.current_item}`; else tCurrent.textContent = 'No active matches'; } - // This worker has no artist/album/track phases — show its matched/pending tally. - if (tProgress && data.stats) { - const s = data.stats; - tProgress.textContent = `${s.matched || 0} matched · ${s.pending || 0} pending`; + // DB-backed artist progress (matches the Manage modal exactly). + if (tProgress) { + const a = (data.progress && data.progress.artists) || {}; + tProgress.textContent = `Artists: ${a.matched || 0} / ${a.total || 0} (${a.percent || 0}%)`; } }