Added Deezer sounce for searching

This commit is contained in:
2026-07-30 08:31:47 -04:00
parent e5643590ee
commit ed34321168
6 changed files with 129 additions and 16 deletions

View File

@@ -35,12 +35,17 @@ class DownloadRequest(BaseModel):
# ── Music search ──────────────────────────────────────────────────────────────
@app.get("/api/search/music")
async def search_music(q: str = Query(..., min_length=1)) -> list[dict[str, Any]]:
"""Search iTunes for song metadata."""
async def search_music(
q: str = Query(..., min_length=1),
source: str = Query("itunes"),
) -> list[dict[str, Any]]:
"""Search for song metadata from the selected source (iTunes or Deezer)."""
if source not in music_search.SOURCES:
raise HTTPException(status_code=400, detail=f"Unknown source: {source}")
try:
return await music_search.search_music(q)
return await music_search.search_music(q, source=source)
except Exception as exc:
raise HTTPException(status_code=502, detail=f"iTunes search failed: {exc}")
raise HTTPException(status_code=502, detail=f"{source} search failed: {exc}")
# ── YouTube search ────────────────────────────────────────────────────────────

View File

@@ -1,15 +1,29 @@
"""
Music metadata search via the iTunes Search API.
Free, no auth, returns track info + album art URLs.
Music metadata search across multiple sources (iTunes, Deezer).
Both are free and require no API key.
"""
import httpx
import asyncio
from typing import Any
import httpx
ITUNES_URL = "https://itunes.apple.com/search"
DEEZER_SEARCH_URL = "https://api.deezer.com/search"
DEEZER_ALBUM_URL = "https://api.deezer.com/album"
SOURCES = ("itunes", "deezer")
async def search_music(query: str, limit: int = 10) -> list[dict[str, Any]]:
async def search_music(query: str, source: str = "itunes", limit: int = 10) -> list[dict[str, Any]]:
if source == "deezer":
return await _search_deezer(query, limit)
return await _search_itunes(query, limit)
# ── iTunes ───────────────────────────────────────────────────────────────────
async def _search_itunes(query: str, limit: int) -> list[dict[str, Any]]:
params = {
"term": query,
"media": "music",
@@ -43,3 +57,57 @@ async def search_music(query: str, limit: int = 10) -> list[dict[str, Any]]:
})
return results
# ── Deezer ───────────────────────────────────────────────────────────────────
async def _search_deezer(query: str, limit: int) -> list[dict[str, Any]]:
async with httpx.AsyncClient(timeout=10) as client:
response = await client.get(DEEZER_SEARCH_URL, params={"q": query, "limit": limit})
response.raise_for_status()
tracks = response.json().get("data", [])
# Deezer's search results don't include genre/release date/track number —
# those live on the album, so fetch each unique album in parallel.
album_ids = {t["album"]["id"] for t in tracks if t.get("album")}
albums = await asyncio.gather(
*(_fetch_deezer_album(client, aid) for aid in album_ids),
return_exceptions=True,
)
album_map = {a["id"]: a for a in albums if isinstance(a, dict) and "id" in a}
results = []
for item in tracks:
album = item.get("album") or {}
album_info = album_map.get(album.get("id"), {})
genres = album_info.get("genres", {}).get("data", [])
release_date = album_info.get("release_date", "")
results.append({
"trackId": item.get("id"),
"trackName": item.get("title", ""),
"artistName": (item.get("artist") or {}).get("name", ""),
"collectionName": album.get("title", ""),
"trackNumber": _deezer_track_position(album_info, item.get("id")),
"discNumber": None,
"releaseDate": release_date[:4] if release_date else "",
"genre": genres[0]["name"] if genres else "",
"artworkUrl": album.get("cover_xl") or album.get("cover_big") or "",
"previewUrl": item.get("preview", ""),
})
return results
async def _fetch_deezer_album(client: httpx.AsyncClient, album_id: int) -> dict[str, Any]:
response = await client.get(f"{DEEZER_ALBUM_URL}/{album_id}")
response.raise_for_status()
return response.json()
def _deezer_track_position(album_info: dict[str, Any], track_id: Any) -> int | None:
for track in album_info.get("tracks", {}).get("data", []):
if track.get("id") == track_id:
return track.get("track_position")
return None