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:
parent
f0a6d5e696
commit
ce8faf0ecc
2 changed files with 44 additions and 0 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Reference in a new issue