discovery: wire adventurousness into "Based On Your Listening" (live, on existing data)

The listening-recs route now reads discovery.adventurousness (default 0.3) and re-ranks the stored recs
through apply_adventurousness before reshaping — so globally-popular picks sink and obscure ones rise.
Popularity is enriched at REQUEST time via a new MusicDatabase.get_similar_artist_popularities(names)
lookup (the stored recs do not carry popularity inline), so it works on existing data with no re-scan.
Both the lookup and the re-rank are fail-soft — any error leaves the original order. Default 0.3 is a
gentle nudge; the slider to tune it is the next increment.

Core pure function already has 5 seam tests; DB lookup smoke-verified against a temp DB.
This commit is contained in:
BoulderBadgeDad 2026-06-29 16:08:20 -07:00
parent f0a6d5e696
commit ce8faf0ecc
2 changed files with 44 additions and 0 deletions

View file

@ -10515,6 +10515,32 @@ class MusicDatabase:
logger.error(f"Error checking similar artists freshness: {e}")
return False # Default to re-fetching on error
def get_similar_artist_popularities(self, names, profile_id: int = 1):
"""Map lowercased artist name -> max stored popularity (0-100) from ``similar_artists`` for
the given profile. Lets the Discover routes apply the adventurousness popularity-penalty at
request time (the stored listening-recs don't carry popularity inline). Fail-soft -> {}."""
out = {}
clean = [str(n).strip().lower() for n in (names or []) if str(n or '').strip()]
if not clean:
return out
try:
with self._get_connection() as conn:
cursor = conn.cursor()
placeholders = ','.join('?' for _ in clean)
cursor.execute(
f"""SELECT LOWER(similar_artist_name) AS n, MAX(popularity) AS pop
FROM similar_artists
WHERE profile_id = ? AND LOWER(similar_artist_name) IN ({placeholders})
GROUP BY LOWER(similar_artist_name)""",
[profile_id] + clean,
)
for row in cursor.fetchall():
if row['pop'] is not None:
out[row['n']] = row['pop']
except Exception as e:
logger.debug(f"get_similar_artist_popularities failed: {e}")
return out
def get_top_similar_artists(
self,
limit: int = 50,

View file

@ -29604,6 +29604,24 @@ def get_discover_listening_recommendations():
except (ValueError, TypeError):
stored = []
# Adventurousness re-rank (aurral-style): enrich popularity at request time (the stored recs
# don't carry it inline, so this works on existing data), then push globally-popular picks
# down per the user's dial. level 0 -> unchanged. Fail-soft: any hiccup leaves the order.
try:
level = float(config_manager.get('discovery.adventurousness', 0.3) or 0)
except (TypeError, ValueError):
level = 0.0
if level > 0 and stored:
try:
pops = database.get_similar_artist_popularities([a.get('name') for a in stored])
for a in stored:
if a.get('popularity') is None:
a['popularity'] = pops.get((a.get('name') or '').strip().lower())
from core.discovery.listening_recommendations import apply_adventurousness
stored = apply_adventurousness(stored, level)
except Exception as _adv_err:
logger.debug(f"adventurousness re-rank skipped: {_adv_err}")
result_artists = []
for a in stored:
name = a.get('name')