feat(spotify): add Spotify search, prioritize Spotify in ranking and recommendations
This commit is contained in:
1 parent
fb1a5e01dd
commit
3db9bdb38b
6 files changed
+207
-14
No files matched your search
+82
-6
@@ -64,6 +64,10 @@ def calculate_track_relevance(query: str, item: Dict[str, Any]) -> float:
|
||||
if tit_norm and (tit_norm == q_norm or (len(q_words) > 1 and q_norm.endswith(tit_norm))):
|
||||
score += 25.0
|
||||
|
||||
# 6. Priority bonus for user's preferred music service: Spotify
|
||||
if item.get("service") == "spotify":
|
||||
score += 25.0
|
||||
|
||||
# Slight penalty for overly long titles (e.g. video descriptions)
|
||||
if len(full_norm) > len(q_norm) * 3:
|
||||
score -= 10.0
|
||||
@@ -302,21 +306,94 @@ async def search_apple(query: str) -> Dict[str, List[Dict[str, Any]]]:
|
||||
print(f"Apple search error: {e}")
|
||||
return {"tracks": tracks, "albums": albums}
|
||||
|
||||
async def search_spotify(query: str) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""Search tracks and albums via Spotify."""
|
||||
loop = asyncio.get_event_loop()
|
||||
return await loop.run_in_executor(None, _search_spotify_sync, query)
|
||||
|
||||
def _search_spotify_sync(query: str) -> Dict[str, List[Dict[str, Any]]]:
|
||||
tracks = []
|
||||
albums = []
|
||||
try:
|
||||
from spotify_scraper import SpotifyClient
|
||||
with SpotifyClient() as client:
|
||||
res = client.search(query, limit=8)
|
||||
if hasattr(res, "tracks") and res.tracks:
|
||||
for t in res.tracks:
|
||||
if not t:
|
||||
continue
|
||||
artists = ", ".join(a.name for a in t.artists) if t.artists else "Unknown"
|
||||
cover = None
|
||||
if hasattr(t, "images") and t.images:
|
||||
img = t.images[0]
|
||||
cover = getattr(img, "url", None) or (img.get("url") if isinstance(img, dict) else None)
|
||||
elif hasattr(t, "album") and t.album and getattr(t.album, "images", None):
|
||||
img = t.album.images[0]
|
||||
cover = getattr(img, "url", None) or (img.get("url") if isinstance(img, dict) else None)
|
||||
|
||||
album_title = getattr(t.album, "name", "Spotify Single") if getattr(t, "album", None) else "Spotify Single"
|
||||
explicit_status = "explicit" if getattr(t, "explicit", False) else "neutral"
|
||||
|
||||
track_url = getattr(t, "url", None) or f"https://open.spotify.com/track/{t.id}"
|
||||
tracks.append({
|
||||
"service": "spotify",
|
||||
"service_name": "Spotify",
|
||||
"id": str(t.id),
|
||||
"title": t.name,
|
||||
"artist": artists,
|
||||
"album": album_title,
|
||||
"year": None,
|
||||
"duration": format_duration(getattr(t, "duration_ms", None)),
|
||||
"cover": cover,
|
||||
"url": track_url,
|
||||
"explicit_status": explicit_status,
|
||||
"version": None,
|
||||
"preview_url": getattr(t, "preview_url", None)
|
||||
})
|
||||
|
||||
if hasattr(res, "albums") and res.albums:
|
||||
for a in res.albums[:4]:
|
||||
if not a:
|
||||
continue
|
||||
cover = None
|
||||
if hasattr(a, "images") and a.images:
|
||||
img = a.images[0]
|
||||
cover = getattr(img, "url", None) or (img.get("url") if isinstance(img, dict) else None)
|
||||
album_url = getattr(a, "url", None) or f"https://open.spotify.com/album/{a.id}"
|
||||
artist_name = "Spotify"
|
||||
if hasattr(a, "artists") and a.artists:
|
||||
artist_name = ", ".join(art.name for art in a.artists)
|
||||
albums.append({
|
||||
"service": "spotify",
|
||||
"service_name": "Spotify",
|
||||
"id": str(a.id),
|
||||
"title": a.name,
|
||||
"artist": artist_name,
|
||||
"year": None,
|
||||
"track_count": getattr(a, "total_tracks", None),
|
||||
"cover": cover,
|
||||
"url": album_url
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Spotify search error: {e}")
|
||||
return {"tracks": tracks, "albums": albums}
|
||||
|
||||
async def search_all(query: str) -> Dict[str, Any]:
|
||||
"""Run concurrent search across all platforms with relevance ranking."""
|
||||
"""Run concurrent search across all platforms (Spotify preferred) with relevance ranking."""
|
||||
sp_task = search_spotify(query)
|
||||
y_task = search_yandex(query)
|
||||
sc_task = search_soundcloud(query)
|
||||
yt_task = search_youtube(query)
|
||||
a_task = search_apple(query)
|
||||
|
||||
y_res, sc_res, yt_res, a_res = await asyncio.gather(
|
||||
y_task, sc_task, yt_task, a_task, return_exceptions=True
|
||||
sp_res, y_res, sc_res, yt_res, a_res = await asyncio.gather(
|
||||
sp_task, y_task, sc_task, yt_task, a_task, return_exceptions=True
|
||||
)
|
||||
|
||||
all_tracks = []
|
||||
all_albums = []
|
||||
|
||||
for res in (y_res, sc_res, yt_res, a_res):
|
||||
for res in (sp_res, y_res, sc_res, yt_res, a_res):
|
||||
if isinstance(res, dict):
|
||||
all_tracks.extend(res.get("tracks", []))
|
||||
all_albums.extend(res.get("albums", []))
|
||||
@@ -325,8 +402,7 @@ async def search_all(query: str) -> Dict[str, Any]:
|
||||
for t in all_tracks:
|
||||
t["relevance_score"] = round(calculate_track_relevance(query, t), 2)
|
||||
|
||||
# Sort tracks descending by relevance score:
|
||||
# Tracks with the highest match (e.g. found on Yandex, SoundCloud, YouTube) appear at the top!
|
||||
# Sort tracks descending by relevance score (Spotify prioritized at top)
|
||||
all_tracks.sort(key=lambda t: t.get("relevance_score", 0), reverse=True)
|
||||
|
||||
# Sort albums by relevance as well
|
||||
|
||||
Reference in new issue
Block a user