Filter owned artists from discovery recommendations

This commit is contained in:
Broque Thomas 2026-05-18 11:40:12 -07:00
parent 04adbf01e2
commit e061f12a05
3 changed files with 144 additions and 5 deletions

View file

@ -8424,9 +8424,16 @@ class MusicDatabase:
logger.error(f"Error checking similar artists freshness: {e}")
return False # Default to re-fetching on error
def get_top_similar_artists(self, limit: int = 50, profile_id: int = 1, require_source: str = None) -> List[SimilarArtist]:
def get_top_similar_artists(
self,
limit: int = 50,
profile_id: int = 1,
require_source: str = None,
exclude_library_server: str = None,
) -> List[SimilarArtist]:
"""Get top similar artists excluding watchlist artists, with cycling support.
require_source: if set ('spotify','itunes','deezer'), only returns artists with that source ID."""
require_source: if set ('spotify','itunes','deezer'), only returns artists with that source ID.
exclude_library_server: if set, also excludes artists already present in that media server."""
try:
with self._get_connection() as conn:
cursor = conn.cursor()
@ -8440,6 +8447,27 @@ class MusicDatabase:
elif require_source == 'deezer':
source_filter = "AND sa.similar_artist_deezer_id IS NOT NULL AND sa.similar_artist_deezer_id != ''"
library_artist_keys = None
sql_limit = limit
if exclude_library_server:
cursor.execute("""
SELECT name, spotify_artist_id, itunes_artist_id, deezer_id
FROM artists
WHERE server_source = ?
""", (exclude_library_server,))
library_rows = cursor.fetchall()
library_artist_keys = {
'spotify': {r['spotify_artist_id'] for r in library_rows if r['spotify_artist_id']},
'itunes': {r['itunes_artist_id'] for r in library_rows if r['itunes_artist_id']},
'deezer': {r['deezer_id'] for r in library_rows if r['deezer_id']},
'names': {
self._normalize_for_comparison(r['name'])
for r in library_rows
if r['name']
},
}
sql_limit = max(limit * 5, limit + 100)
cursor.execute(f"""
SELECT
MAX(sa.id) as id,
@ -8469,11 +8497,24 @@ class MusicDatabase:
occurrence_count DESC,
similarity_rank ASC
LIMIT ?
""", (profile_id, profile_id, limit))
""", (profile_id, profile_id, sql_limit))
rows = cursor.fetchall()
results = []
for row in rows:
if library_artist_keys:
spotify_id = row['similar_artist_spotify_id']
itunes_id = row['similar_artist_itunes_id'] if 'similar_artist_itunes_id' in row.keys() else None
deezer_id = row['similar_artist_deezer_id'] if 'similar_artist_deezer_id' in row.keys() else None
normalized_name = self._normalize_for_comparison(row['similar_artist_name'])
if (
(spotify_id and spotify_id in library_artist_keys['spotify'])
or (itunes_id and itunes_id in library_artist_keys['itunes'])
or (deezer_id and deezer_id in library_artist_keys['deezer'])
or (normalized_name and normalized_name in library_artist_keys['names'])
):
continue
genres_raw = row['genres'] if 'genres' in row.keys() else None
try:
genres_list = json.loads(genres_raw) if genres_raw else None
@ -8493,6 +8534,8 @@ class MusicDatabase:
genres=genres_list,
popularity=row['popularity'] if 'popularity' in row.keys() else 0,
))
if len(results) >= limit:
break
return results
except Exception as e:

View file

@ -0,0 +1,86 @@
from database.music_database import MusicDatabase
def _names(artists):
return {artist.similar_artist_name for artist in artists}
def test_top_similar_artists_can_exclude_active_server_library_artists(tmp_path):
db = MusicDatabase(str(tmp_path / "music.db"))
db.add_or_update_similar_artist(
source_artist_id="seed-1",
similar_artist_name="Owned By Spotify ID",
similar_artist_spotify_id="sp-owned",
profile_id=1,
)
db.add_or_update_similar_artist(
source_artist_id="seed-1",
similar_artist_name="Owned By Deezer ID",
similar_artist_deezer_id="dz-owned",
profile_id=1,
)
db.add_or_update_similar_artist(
source_artist_id="seed-1",
similar_artist_name="Owned By Name",
similar_artist_spotify_id="sp-owned-name",
profile_id=1,
)
db.add_or_update_similar_artist(
source_artist_id="seed-1",
similar_artist_name="Different Server Artist",
similar_artist_spotify_id="sp-other-server",
profile_id=1,
)
db.add_or_update_similar_artist(
source_artist_id="seed-1",
similar_artist_name="Fresh Artist",
similar_artist_spotify_id="sp-fresh",
profile_id=1,
)
with db._get_connection() as conn:
conn.executemany(
"""
INSERT INTO artists (name, server_source, spotify_artist_id, deezer_id)
VALUES (?, ?, ?, ?)
""",
[
("Library Alias", "navidrome", "sp-owned", None),
("Library Deezer Alias", "navidrome", None, "dz-owned"),
("owned by name", "navidrome", None, None),
("Different Server Artist", "plex", "sp-other-server", None),
],
)
conn.commit()
artists = db.get_top_similar_artists(
limit=20,
profile_id=1,
exclude_library_server="navidrome",
)
assert _names(artists) == {"Different Server Artist", "Fresh Artist"}
def test_top_similar_artists_keeps_existing_behavior_without_library_filter(tmp_path):
db = MusicDatabase(str(tmp_path / "music.db"))
db.add_or_update_similar_artist(
source_artist_id="seed-1",
similar_artist_name="Owned Artist",
similar_artist_spotify_id="sp-owned",
profile_id=1,
)
with db._get_connection() as conn:
conn.execute(
"""
INSERT INTO artists (name, server_source, spotify_artist_id)
VALUES (?, ?, ?)
""",
("Owned Artist", "navidrome", "sp-owned"),
)
conn.commit()
artists = db.get_top_similar_artists(limit=20, profile_id=1)
assert _names(artists) == {"Owned Artist"}

View file

@ -25880,8 +25880,15 @@ def get_discover_similar_artists():
try:
database = get_database()
active_source = _get_active_discovery_source()
from config.settings import config_manager
active_server = config_manager.get_active_media_server()
similar_artists = database.get_top_similar_artists(limit=200, profile_id=get_current_profile_id(), require_source=active_source)
similar_artists = database.get_top_similar_artists(
limit=200,
profile_id=get_current_profile_id(),
require_source=active_source,
exclude_library_server=active_server,
)
if not similar_artists:
return jsonify({"success": True, "artists": [], "source": active_source, "count": 0})
@ -25915,7 +25922,10 @@ def get_discover_similar_artists():
artist_data["popularity"] = artist.popularity
result_artists.append(artist_data)
logger.info(f"[Similar Artists] {len(similar_artists)} from DB, {len(result_artists)} valid for {active_source}")
logger.info(
f"[Similar Artists] {len(similar_artists)} from DB, {len(result_artists)} valid for "
f"{active_source} after excluding {active_server} library artists"
)
return jsonify({
"success": True,