Add hero cycling for similar artists
Add support for cycling hero/featured similar artists by introducing a last_featured timestamp and using it to prefer least-recently-featured artists. Changes include: - DB migration: add _add_similar_artists_last_featured_column and call it during migrations to add a last_featured TIMESTAMP column (non-fatal on error). - Query changes: get_top_similar_artists now excludes watchlist artists via a LEFT JOIN and wa.id IS NULL and orders results by last_featured (nulls first), then by last_featured asc, occurrence_count desc, and similarity_rank asc. The query aliasing was also added for clarity. - New helper: mark_artists_featured updates similar_artists.last_featured = CURRENT_TIMESTAMP for shown artists. - Web server: increase fetch limit to 50, remove random shuffle and instead take the top 10 (already ordered by cycling logic), and call mark_artists_featured to rotate featured artists. These changes aim to provide deterministic, least-recently-shown cycling of hero artists while keeping watchlist artists out of recommendations.
This commit is contained in:
parent
f90d6567f7
commit
7261b04950
2 changed files with 64 additions and 19 deletions
|
|
@ -325,6 +325,9 @@ class MusicDatabase:
|
|||
)
|
||||
""")
|
||||
|
||||
# Add last_featured column to similar_artists for hero cycling (migration)
|
||||
self._add_similar_artists_last_featured_column(cursor)
|
||||
|
||||
# Retag tool tables for tracking processed downloads (migration)
|
||||
self._add_retag_tables(cursor)
|
||||
|
||||
|
|
@ -877,6 +880,20 @@ class MusicDatabase:
|
|||
logger.error(f"Error adding itunes_artist_id column to watchlist_artists: {e}")
|
||||
# Don't raise - this is a migration, database can still function
|
||||
|
||||
def _add_similar_artists_last_featured_column(self, cursor):
|
||||
"""Add last_featured column to similar_artists for hero slider cycling"""
|
||||
try:
|
||||
cursor.execute("PRAGMA table_info(similar_artists)")
|
||||
columns = [column[1] for column in cursor.fetchall()]
|
||||
|
||||
if 'last_featured' not in columns:
|
||||
cursor.execute("ALTER TABLE similar_artists ADD COLUMN last_featured TIMESTAMP")
|
||||
logger.info("Added last_featured column to similar_artists table for hero cycling")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding last_featured column to similar_artists: {e}")
|
||||
# Don't raise - this is a migration, database can still function
|
||||
|
||||
def _fix_watchlist_spotify_id_nullable(self, cursor):
|
||||
"""
|
||||
Make spotify_artist_id nullable in watchlist_artists table.
|
||||
|
|
@ -4124,24 +4141,34 @@ class MusicDatabase:
|
|||
return False # Default to re-fetching on error
|
||||
|
||||
def get_top_similar_artists(self, limit: int = 50) -> List[SimilarArtist]:
|
||||
"""Get top similar artists across all watchlist artists, ordered by occurrence count"""
|
||||
"""Get top similar artists excluding watchlist artists, with cycling support"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
MAX(id) as id,
|
||||
MAX(source_artist_id) as source_artist_id,
|
||||
MAX(similar_artist_spotify_id) as similar_artist_spotify_id,
|
||||
MAX(similar_artist_itunes_id) as similar_artist_itunes_id,
|
||||
similar_artist_name,
|
||||
AVG(similarity_rank) as similarity_rank,
|
||||
SUM(occurrence_count) as occurrence_count,
|
||||
MAX(last_updated) as last_updated
|
||||
FROM similar_artists
|
||||
GROUP BY similar_artist_name
|
||||
ORDER BY occurrence_count DESC, similarity_rank ASC
|
||||
MAX(sa.id) as id,
|
||||
MAX(sa.source_artist_id) as source_artist_id,
|
||||
MAX(sa.similar_artist_spotify_id) as similar_artist_spotify_id,
|
||||
MAX(sa.similar_artist_itunes_id) as similar_artist_itunes_id,
|
||||
sa.similar_artist_name,
|
||||
AVG(sa.similarity_rank) as similarity_rank,
|
||||
SUM(sa.occurrence_count) as occurrence_count,
|
||||
MAX(sa.last_updated) as last_updated
|
||||
FROM similar_artists sa
|
||||
LEFT JOIN watchlist_artists wa ON (
|
||||
(sa.similar_artist_spotify_id IS NOT NULL AND sa.similar_artist_spotify_id = wa.spotify_artist_id)
|
||||
OR (sa.similar_artist_itunes_id IS NOT NULL AND sa.similar_artist_itunes_id = wa.itunes_artist_id)
|
||||
OR LOWER(sa.similar_artist_name) = LOWER(wa.artist_name)
|
||||
)
|
||||
WHERE wa.id IS NULL
|
||||
GROUP BY sa.similar_artist_name
|
||||
ORDER BY
|
||||
CASE WHEN MAX(sa.last_featured) IS NULL THEN 0 ELSE 1 END,
|
||||
MAX(sa.last_featured) ASC,
|
||||
occurrence_count DESC,
|
||||
similarity_rank ASC
|
||||
LIMIT ?
|
||||
""", (limit,))
|
||||
|
||||
|
|
@ -4161,6 +4188,23 @@ class MusicDatabase:
|
|||
logger.error(f"Error getting top similar artists: {e}")
|
||||
return []
|
||||
|
||||
def mark_artists_featured(self, artist_names: List[str]):
|
||||
"""Update last_featured timestamp for artists shown in the hero slider"""
|
||||
if not artist_names:
|
||||
return
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
placeholders = ','.join('?' for _ in artist_names)
|
||||
cursor.execute(f"""
|
||||
UPDATE similar_artists
|
||||
SET last_featured = CURRENT_TIMESTAMP
|
||||
WHERE similar_artist_name IN ({placeholders})
|
||||
""", artist_names)
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
logger.error(f"Error marking artists as featured: {e}")
|
||||
|
||||
def add_to_discovery_pool(self, track_data: Dict[str, Any], source: str = 'spotify') -> bool:
|
||||
"""Add a track to the discovery pool (supports both Spotify and iTunes sources)"""
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -22031,8 +22031,8 @@ def get_discover_hero():
|
|||
from core.itunes_client import iTunesClient
|
||||
itunes_client = iTunesClient()
|
||||
|
||||
# Get top similar artists (by occurrence count) - get 20 for variety
|
||||
similar_artists = database.get_top_similar_artists(limit=20)
|
||||
# Get top similar artists (excluding watchlist, cycled by last_featured)
|
||||
similar_artists = database.get_top_similar_artists(limit=50)
|
||||
|
||||
# FALLBACK: If no similar artists exist, use watchlist artists for Hero section
|
||||
if not similar_artists:
|
||||
|
|
@ -22129,11 +22129,8 @@ def get_discover_hero():
|
|||
|
||||
print(f"[Discover Hero] Found {len(valid_artists)} valid artists for source: {active_source}")
|
||||
|
||||
# Shuffle for variety and take top 10
|
||||
import random
|
||||
shuffled = list(valid_artists)
|
||||
random.shuffle(shuffled)
|
||||
similar_artists = shuffled[:10]
|
||||
# Take top 10 (already ordered by least-recently-featured, then quality)
|
||||
similar_artists = valid_artists[:10]
|
||||
|
||||
# Convert to JSON format with data enrichment from appropriate source
|
||||
hero_artists = []
|
||||
|
|
@ -22178,6 +22175,10 @@ def get_discover_hero():
|
|||
|
||||
hero_artists.append(artist_data)
|
||||
|
||||
# Mark these artists as featured so they cycle to the back of the queue
|
||||
featured_names = [a["artist_name"] for a in hero_artists]
|
||||
database.mark_artists_featured(featured_names)
|
||||
|
||||
return jsonify({"success": True, "artists": hero_artists, "source": active_source})
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
|||
Loading…
Reference in a new issue