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}")
|
logger.error(f"Error checking similar artists freshness: {e}")
|
||||||
return False # Default to re-fetching on error
|
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.
|
"""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:
|
try:
|
||||||
with self._get_connection() as conn:
|
with self._get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
@ -8440,6 +8447,27 @@ class MusicDatabase:
|
||||||
elif require_source == 'deezer':
|
elif require_source == 'deezer':
|
||||||
source_filter = "AND sa.similar_artist_deezer_id IS NOT NULL AND sa.similar_artist_deezer_id != ''"
|
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"""
|
cursor.execute(f"""
|
||||||
SELECT
|
SELECT
|
||||||
MAX(sa.id) as id,
|
MAX(sa.id) as id,
|
||||||
|
|
@ -8469,11 +8497,24 @@ class MusicDatabase:
|
||||||
occurrence_count DESC,
|
occurrence_count DESC,
|
||||||
similarity_rank ASC
|
similarity_rank ASC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
""", (profile_id, profile_id, limit))
|
""", (profile_id, profile_id, sql_limit))
|
||||||
|
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
results = []
|
results = []
|
||||||
for row in rows:
|
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
|
genres_raw = row['genres'] if 'genres' in row.keys() else None
|
||||||
try:
|
try:
|
||||||
genres_list = json.loads(genres_raw) if genres_raw else None
|
genres_list = json.loads(genres_raw) if genres_raw else None
|
||||||
|
|
@ -8493,6 +8534,8 @@ class MusicDatabase:
|
||||||
genres=genres_list,
|
genres=genres_list,
|
||||||
popularity=row['popularity'] if 'popularity' in row.keys() else 0,
|
popularity=row['popularity'] if 'popularity' in row.keys() else 0,
|
||||||
))
|
))
|
||||||
|
if len(results) >= limit:
|
||||||
|
break
|
||||||
return results
|
return results
|
||||||
|
|
||||||
except Exception as e:
|
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:
|
try:
|
||||||
database = get_database()
|
database = get_database()
|
||||||
active_source = _get_active_discovery_source()
|
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:
|
if not similar_artists:
|
||||||
return jsonify({"success": True, "artists": [], "source": active_source, "count": 0})
|
return jsonify({"success": True, "artists": [], "source": active_source, "count": 0})
|
||||||
|
|
@ -25915,7 +25922,10 @@ def get_discover_similar_artists():
|
||||||
artist_data["popularity"] = artist.popularity
|
artist_data["popularity"] = artist.popularity
|
||||||
result_artists.append(artist_data)
|
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({
|
return jsonify({
|
||||||
"success": True,
|
"success": True,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue