82 lines
3.0 KiB
Python
82 lines
3.0 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, Optional
|
|
|
|
from .HTTP import HTTPClient
|
|
|
|
DEFAULT_BASE_URL = "https://tidal-api.binimum.org"
|
|
|
|
|
|
class HifiApiError(Exception):
|
|
"""Raised when the HiFi API returns an error or malformed response."""
|
|
|
|
|
|
class HifiApiClient:
|
|
"""Lightweight client for the hifi-api endpoints.
|
|
|
|
Supported endpoints:
|
|
- GET /search/ with exactly one of s, a, v, p
|
|
- GET /track/ with id (and optional quality)
|
|
- GET /info/ with id
|
|
- GET /lyrics/ with id
|
|
"""
|
|
|
|
def __init__(self, base_url: str = DEFAULT_BASE_URL, *, timeout: float = 10.0) -> None:
|
|
self.base_url = str(base_url or DEFAULT_BASE_URL).rstrip("/")
|
|
self.timeout = float(timeout)
|
|
|
|
def _get_json(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
url = f"{self.base_url}/{str(path or '').lstrip('/')}"
|
|
with HTTPClient(timeout=self.timeout) as client:
|
|
response = client.get(url, params=params, allow_redirects=True)
|
|
response.raise_for_status()
|
|
try:
|
|
return response.json()
|
|
except Exception as exc: # pragma: no cover - defensive
|
|
raise HifiApiError(f"Invalid JSON response from {url}: {exc}") from exc
|
|
|
|
def search(self, params: Dict[str, str]) -> Dict[str, Any]:
|
|
usable = {k: v for k, v in (params or {}).items() if v}
|
|
search_keys = [key for key in ("s", "a", "v", "p") if usable.get(key)]
|
|
if not search_keys:
|
|
raise HifiApiError("One of s/a/v/p is required for /search/")
|
|
if len(search_keys) > 1:
|
|
first = search_keys[0]
|
|
usable = {first: usable[first]}
|
|
return self._get_json("search/", params=usable)
|
|
|
|
def track(self, track_id: int, *, quality: Optional[str] = None) -> Dict[str, Any]:
|
|
try:
|
|
track_int = int(track_id)
|
|
except Exception as exc:
|
|
raise HifiApiError(f"track_id must be int-compatible: {exc}") from exc
|
|
if track_int <= 0:
|
|
raise HifiApiError("track_id must be positive")
|
|
|
|
params: Dict[str, Any] = {"id": track_int}
|
|
if quality:
|
|
params["quality"] = str(quality)
|
|
return self._get_json("track/", params=params)
|
|
|
|
def info(self, track_id: int) -> Dict[str, Any]:
|
|
try:
|
|
track_int = int(track_id)
|
|
except Exception as exc:
|
|
raise HifiApiError(f"track_id must be int-compatible: {exc}") from exc
|
|
if track_int <= 0:
|
|
raise HifiApiError("track_id must be positive")
|
|
|
|
return self._get_json("info/", params={"id": track_int})
|
|
|
|
def lyrics(self, track_id: int) -> Dict[str, Any]:
|
|
"""Fetch lyrics (including subtitles/LRC) for a track."""
|
|
|
|
try:
|
|
track_int = int(track_id)
|
|
except Exception as exc:
|
|
raise HifiApiError(f"track_id must be int-compatible: {exc}") from exc
|
|
if track_int <= 0:
|
|
raise HifiApiError("track_id must be positive")
|
|
|
|
return self._get_json("lyrics/", params={"id": track_int})
|