Filter owned artists from discovery recommendations
This commit is contained in:
parent
04adbf01e2
commit
e061f12a05
3 changed files with 144 additions and 5 deletions
|
|
@ -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:
|
||||
|
|
|
|||
86
tests/discovery/test_similar_artists_library_filter.py
Normal file
86
tests/discovery/test_similar_artists_library_filter.py
Normal 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"}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue