fix(amazon): search albums/artists and track numbers for t2tunes
t2tunes uses HTTP 400 for transient Amazon-side failures instead of 5xx. The first API call in a fresh session hit this every time, so album and artist searches always failed while the track search (called 0.5 s later) got through. - _get_json: retry up to 3 times (1 s, 2 s backoff) on t2tunes-specific 400 "Failed to search" responses - All search_raw calls switched from types="track,album" to types="track" — t2tunes album-type queries are currently broken server-side; albums and artists are now derived from track result metadata instead - search_albums: drop is_album filter, extract album fields from track hits - get_album_tracks: fall back to stream index (1-based) when t2tunes tags omit trackNumber, preventing every track landing as track 01
This commit is contained in:
parent
1575ba4684
commit
478bcc5d3b
2 changed files with 40 additions and 26 deletions
|
|
@ -327,7 +327,7 @@ class AmazonClient:
|
|||
|
||||
def search_tracks(self, query: str, limit: int = 20) -> List[Track]:
|
||||
_rate_limit()
|
||||
items = self.search_raw(query, types="track,album")
|
||||
items = self.search_raw(query, types="track")
|
||||
track_pairs: List[tuple] = [] # (Track, album_asin)
|
||||
seen_album_asins: List[str] = []
|
||||
for item in items:
|
||||
|
|
@ -360,7 +360,7 @@ class AmazonClient:
|
|||
|
||||
def search_artists(self, query: str, limit: int = 20) -> List[Artist]:
|
||||
_rate_limit()
|
||||
items = self.search_raw(query, types="track,album")
|
||||
items = self.search_raw(query, types="track")
|
||||
seen: Dict[str, Artist] = {}
|
||||
artist_album_asin: Dict[str, str] = {} # artist name → first album ASIN seen
|
||||
for item in items:
|
||||
|
|
@ -384,14 +384,14 @@ class AmazonClient:
|
|||
|
||||
def search_albums(self, query: str, limit: int = 20) -> List[Album]:
|
||||
_rate_limit()
|
||||
items = self.search_raw(query, types="track,album")
|
||||
items = self.search_raw(query, types="track")
|
||||
album_candidates: List[tuple] = [] # (Album, asin)
|
||||
seen_keys: set = set()
|
||||
for item in items:
|
||||
if not item.is_album:
|
||||
album_asin = item.album_asin
|
||||
raw_name = item.album_name
|
||||
if not raw_name:
|
||||
continue
|
||||
album_asin = item.album_asin or item.asin
|
||||
raw_name = item.album_name or item.title
|
||||
display_name = _strip_edition(raw_name)
|
||||
artist = _primary_artist(item.artist_name)
|
||||
# Collapse Explicit/Clean variants: same normalised name + artist = same album
|
||||
|
|
@ -519,7 +519,7 @@ class AmazonClient:
|
|||
artist_name = _primary_artist(streams[0].artist)
|
||||
try:
|
||||
search_items = self.search_raw(
|
||||
f"{album_name} {artist_name}", types="track,album"
|
||||
f"{album_name} {artist_name}", types="track"
|
||||
)
|
||||
for item in search_items:
|
||||
if item.album_asin == asin and item.duration_seconds:
|
||||
|
|
@ -533,12 +533,12 @@ class AmazonClient:
|
|||
"name": _strip_edition(s.title),
|
||||
"artists": [{"name": _primary_artist(s.artist), "id": ""}],
|
||||
"duration_ms": duration_map.get(s.asin, 0),
|
||||
"track_number": s.track_number,
|
||||
"track_number": s.track_number if s.track_number is not None else idx + 1,
|
||||
"disc_number": s.disc_number,
|
||||
"release_date": s.date or "",
|
||||
"isrc": s.isrc,
|
||||
}
|
||||
for s in streams
|
||||
for idx, s in enumerate(streams)
|
||||
]
|
||||
return {"items": items, "total": len(items), "limit": 50, "next": None}
|
||||
|
||||
|
|
@ -547,7 +547,7 @@ class AmazonClient:
|
|||
_rate_limit()
|
||||
search_name = _unslugify(artist_name)
|
||||
try:
|
||||
items = self.search_raw(search_name, types="track,album")
|
||||
items = self.search_raw(search_name, types="track")
|
||||
except AmazonClientError:
|
||||
return None
|
||||
name_lower = search_name.lower()
|
||||
|
|
@ -577,7 +577,7 @@ class AmazonClient:
|
|||
_rate_limit()
|
||||
search_name = _unslugify(artist_name)
|
||||
try:
|
||||
items = self.search_raw(f"{search_name} album", types="track,album")
|
||||
items = self.search_raw(f"{search_name} album", types="track")
|
||||
except AmazonClientError:
|
||||
return []
|
||||
album_candidates: List[tuple] = [] # (Album, asin)
|
||||
|
|
@ -630,7 +630,7 @@ class AmazonClient:
|
|||
"""Return an album cover as artist image stand-in (T2Tunes has no artist images)."""
|
||||
search_name = _unslugify(artist_id)
|
||||
try:
|
||||
items = self.search_raw(search_name, types="track,album")
|
||||
items = self.search_raw(search_name, types="track")
|
||||
except AmazonClientError:
|
||||
return None
|
||||
name_lower = search_name.lower()
|
||||
|
|
@ -686,20 +686,33 @@ class AmazonClient:
|
|||
|
||||
def _get_json(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
||||
url = urljoin(f"{self.base_url}/", path.lstrip("/"))
|
||||
try:
|
||||
resp = self.session.get(url, params=params, timeout=self.timeout)
|
||||
resp.raise_for_status()
|
||||
except requests.HTTPError as exc:
|
||||
raise AmazonClientError(
|
||||
f"HTTP {exc.response.status_code} for {url}"
|
||||
) from exc
|
||||
except requests.RequestException as exc:
|
||||
raise AmazonClientError(f"Request failed for {url}: {exc}") from exc
|
||||
try:
|
||||
return resp.json()
|
||||
except ValueError as exc:
|
||||
preview = resp.text[:200].replace("\n", " ")
|
||||
raise AmazonClientError(f"Response not JSON for {url}: {preview!r}") from exc
|
||||
last_exc: Optional[Exception] = None
|
||||
for attempt in range(3):
|
||||
if attempt:
|
||||
time.sleep(1.0 * attempt)
|
||||
try:
|
||||
resp = self.session.get(url, params=params, timeout=self.timeout)
|
||||
resp.raise_for_status()
|
||||
except requests.HTTPError as exc:
|
||||
body = exc.response.text[:500].replace("\n", " ")
|
||||
# T2Tunes returns 400 for transient Amazon-side failures — retry those.
|
||||
if exc.response.status_code == 400 and "Failed to search" in exc.response.text:
|
||||
logger.debug("T2Tunes transient 400 on attempt %d, retrying: %s", attempt + 1, url)
|
||||
last_exc = AmazonClientError(
|
||||
f"HTTP {exc.response.status_code} for {url} — body: {body!r}"
|
||||
)
|
||||
continue
|
||||
raise AmazonClientError(
|
||||
f"HTTP {exc.response.status_code} for {url} — body: {body!r}"
|
||||
) from exc
|
||||
except requests.RequestException as exc:
|
||||
raise AmazonClientError(f"Request failed for {url}: {exc}") from exc
|
||||
try:
|
||||
return resp.json()
|
||||
except ValueError as exc:
|
||||
preview = resp.text[:200].replace("\n", " ")
|
||||
raise AmazonClientError(f"Response not JSON for {url}: {preview!r}") from exc
|
||||
raise last_exc # type: ignore[misc]
|
||||
|
||||
@staticmethod
|
||||
def _iter_search_items(response: Any) -> Iterator[T2TunesSearchItem]:
|
||||
|
|
|
|||
|
|
@ -3418,6 +3418,7 @@ const WHATS_NEW = {
|
|||
{ title: 'MusicBrainz as Primary Metadata Source', desc: 'MusicBrainz is now a full primary metadata source on equal footing with Deezer, iTunes, Spotify, and Discogs. switch to it in Settings → Metadata Source — always available, no account or API key needed, rate-limited to 1 req/sec. covers all primary source flows: search, album/track/artist lookup, watchlist scans, discover hero, similar artist backfill, artist map.', page: 'settings' },
|
||||
{ title: 'Fix: MusicBrainz artist detail showing MBID as name', desc: 'clicking a MusicBrainz artist from search results was showing the raw MBID as the artist name on the detail page. URL-driven routing (PR #644) no longer passes the display name to the backend, so the source detail endpoint now looks it up directly from MusicBrainz by MBID.' },
|
||||
{ title: 'Fix: artist detail back button always showing "← Back"', desc: 'PR #644 removed the back-button label logic along with the origin stack. restored: a label stack (separate from browser history, which handles actual navigation) tracks where you came from across the full similar-artist chain — "← Back to Search", "← Back to Artist A", "← Back to Artist B", etc. API response backfills the current artist name so the stack has real names when clicking similar artists.' },
|
||||
{ title: 'Fix: Amazon search albums/artists missing, album downloads all track 01', desc: 't2tunes proxies Amazon Music and uses 400 to signal transient failures — first API call in a session hit this consistently, so album/artist searches always failed while track search (called 0.5s later) scraped through. added up to 3 retries with backoff on t2tunes-specific 400s. also: all search methods were using types=track,album but t2tunes album-type queries are broken — switched everything to types=track and derive albums/artists from track metadata instead. track numbers from album downloads were also always 1 — added index-based fallback when t2tunes tags omit trackNumber.' },
|
||||
],
|
||||
'2.5.5': [
|
||||
{ date: 'May 17, 2026 — 2.5.5 release' },
|
||||
|
|
|
|||
Loading…
Reference in a new issue