feat: add real streamrip Qobuz client and opt-in live test
This commit is contained in:
@@ -0,0 +1,82 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
|
||||||
|
class StreamripClient:
|
||||||
|
"""Real Qobuz client backed by streamrip (v2, async → sync via asyncio.run).
|
||||||
|
|
||||||
|
NOT unit-tested offline; see test_qobuz_live.py. streamrip is imported lazily
|
||||||
|
inside the async helpers so importing/constructing this class stays offline.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config: dict):
|
||||||
|
self._email = config.get("qobuz.email", "")
|
||||||
|
self._password = config.get("qobuz.password", "")
|
||||||
|
|
||||||
|
def is_configured(self) -> bool:
|
||||||
|
return bool(self._email and self._password)
|
||||||
|
|
||||||
|
def _make_config(self, download_folder: str | None = None):
|
||||||
|
from streamrip.config import Config
|
||||||
|
|
||||||
|
cfg = Config.defaults()
|
||||||
|
cfg.session.qobuz.use_auth_token = False
|
||||||
|
cfg.session.qobuz.email_or_userid = self._email
|
||||||
|
cfg.session.qobuz.password_or_token = self._password
|
||||||
|
cfg.session.qobuz.quality = 3
|
||||||
|
if download_folder is not None:
|
||||||
|
cfg.session.downloads.folder = download_folder
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
def search_album(self, artist: str, album: str) -> list[dict]:
|
||||||
|
return asyncio.run(self._search(f"{artist} {album}"))
|
||||||
|
|
||||||
|
async def _search(self, query: str) -> list[dict]:
|
||||||
|
from streamrip.client import QobuzClient
|
||||||
|
|
||||||
|
client = QobuzClient(self._make_config())
|
||||||
|
await client.login()
|
||||||
|
try:
|
||||||
|
pages = await client.search("album", query, limit=5)
|
||||||
|
finally:
|
||||||
|
await client.session.close()
|
||||||
|
results: list[dict] = []
|
||||||
|
for page in pages:
|
||||||
|
for item in page.get("albums", {}).get("items", []):
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"source_ref": str(item.get("id", "")),
|
||||||
|
"title": item.get("title", ""),
|
||||||
|
"artist": (item.get("artist") or {}).get("name", ""),
|
||||||
|
"track_count": item.get("tracks_count", 0) or 0,
|
||||||
|
"bit_depth": item.get("maximum_bit_depth", 16) or 16,
|
||||||
|
"sampling_rate": item.get("maximum_sampling_rate", 44.1) or 44.1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def download(self, source_ref: str, dest: str, on_progress: Callable[[float], None]) -> dict:
|
||||||
|
return asyncio.run(self._download(source_ref, dest, on_progress))
|
||||||
|
|
||||||
|
async def _download(self, album_id: str, dest: str, on_progress: Callable[[float], None]) -> dict:
|
||||||
|
os.makedirs(dest, exist_ok=True)
|
||||||
|
from streamrip.client import QobuzClient
|
||||||
|
from streamrip.db import Database, Dummy
|
||||||
|
from streamrip.media import PendingAlbum
|
||||||
|
|
||||||
|
client = QobuzClient(self._make_config(download_folder=dest))
|
||||||
|
await client.login()
|
||||||
|
try:
|
||||||
|
on_progress(0.0)
|
||||||
|
db = Database(downloads=Dummy(), failed=Dummy())
|
||||||
|
pending = PendingAlbum(album_id, client, self._make_config(download_folder=dest), db)
|
||||||
|
album = await pending.resolve()
|
||||||
|
if album is None:
|
||||||
|
raise RuntimeError(f"could not resolve Qobuz album {album_id}")
|
||||||
|
await album.rip()
|
||||||
|
track_count = len(getattr(album, "tracks", []) or [])
|
||||||
|
on_progress(1.0)
|
||||||
|
finally:
|
||||||
|
await client.session.close()
|
||||||
|
return {"track_count": track_count, "path": dest}
|
||||||
@@ -2,3 +2,4 @@ psycopg[binary]>=3.2,<4
|
|||||||
pytest>=8.3,<9
|
pytest>=8.3,<9
|
||||||
cryptography>=42,<46
|
cryptography>=42,<46
|
||||||
yt-dlp>=2024.1
|
yt-dlp>=2024.1
|
||||||
|
streamrip>=2.0
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from lyra_worker.adapters._streamrip import StreamripClient
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipif(
|
||||||
|
not (os.environ.get("LYRA_LIVE_TESTS") and os.environ.get("QOBUZ_EMAIL") and os.environ.get("QOBUZ_PASSWORD")),
|
||||||
|
reason="live Qobuz test; set LYRA_LIVE_TESTS=1 + QOBUZ_EMAIL + QOBUZ_PASSWORD to run",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_live_search_returns_results():
|
||||||
|
client = StreamripClient(
|
||||||
|
{"qobuz.email": os.environ["QOBUZ_EMAIL"], "qobuz.password": os.environ["QOBUZ_PASSWORD"]}
|
||||||
|
)
|
||||||
|
results = client.search_album("John Mayer", "Continuum")
|
||||||
|
assert len(results) >= 1
|
||||||
|
assert all(r["source_ref"] for r in results)
|
||||||
|
# sanity: at least one result is a plausible lossless match
|
||||||
|
assert any(r["bit_depth"] >= 16 for r in results)
|
||||||
Reference in New Issue
Block a user