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]:
|
def search_tracks(self, query: str, limit: int = 20) -> List[Track]:
|
||||||
_rate_limit()
|
_rate_limit()
|
||||||
items = self.search_raw(query, types="track,album")
|
items = self.search_raw(query, types="track")
|
||||||
track_pairs: List[tuple] = [] # (Track, album_asin)
|
track_pairs: List[tuple] = [] # (Track, album_asin)
|
||||||
seen_album_asins: List[str] = []
|
seen_album_asins: List[str] = []
|
||||||
for item in items:
|
for item in items:
|
||||||
|
|
@ -360,7 +360,7 @@ class AmazonClient:
|
||||||
|
|
||||||
def search_artists(self, query: str, limit: int = 20) -> List[Artist]:
|
def search_artists(self, query: str, limit: int = 20) -> List[Artist]:
|
||||||
_rate_limit()
|
_rate_limit()
|
||||||
items = self.search_raw(query, types="track,album")
|
items = self.search_raw(query, types="track")
|
||||||
seen: Dict[str, Artist] = {}
|
seen: Dict[str, Artist] = {}
|
||||||
artist_album_asin: Dict[str, str] = {} # artist name → first album ASIN seen
|
artist_album_asin: Dict[str, str] = {} # artist name → first album ASIN seen
|
||||||
for item in items:
|
for item in items:
|
||||||
|
|
@ -384,14 +384,14 @@ class AmazonClient:
|
||||||
|
|
||||||
def search_albums(self, query: str, limit: int = 20) -> List[Album]:
|
def search_albums(self, query: str, limit: int = 20) -> List[Album]:
|
||||||
_rate_limit()
|
_rate_limit()
|
||||||
items = self.search_raw(query, types="track,album")
|
items = self.search_raw(query, types="track")
|
||||||
album_candidates: List[tuple] = [] # (Album, asin)
|
album_candidates: List[tuple] = [] # (Album, asin)
|
||||||
seen_keys: set = set()
|
seen_keys: set = set()
|
||||||
for item in items:
|
for item in items:
|
||||||
if not item.is_album:
|
album_asin = item.album_asin
|
||||||
|
raw_name = item.album_name
|
||||||
|
if not raw_name:
|
||||||
continue
|
continue
|
||||||
album_asin = item.album_asin or item.asin
|
|
||||||
raw_name = item.album_name or item.title
|
|
||||||
display_name = _strip_edition(raw_name)
|
display_name = _strip_edition(raw_name)
|
||||||
artist = _primary_artist(item.artist_name)
|
artist = _primary_artist(item.artist_name)
|
||||||
# Collapse Explicit/Clean variants: same normalised name + artist = same album
|
# Collapse Explicit/Clean variants: same normalised name + artist = same album
|
||||||
|
|
@ -519,7 +519,7 @@ class AmazonClient:
|
||||||
artist_name = _primary_artist(streams[0].artist)
|
artist_name = _primary_artist(streams[0].artist)
|
||||||
try:
|
try:
|
||||||
search_items = self.search_raw(
|
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:
|
for item in search_items:
|
||||||
if item.album_asin == asin and item.duration_seconds:
|
if item.album_asin == asin and item.duration_seconds:
|
||||||
|
|
@ -533,12 +533,12 @@ class AmazonClient:
|
||||||
"name": _strip_edition(s.title),
|
"name": _strip_edition(s.title),
|
||||||
"artists": [{"name": _primary_artist(s.artist), "id": ""}],
|
"artists": [{"name": _primary_artist(s.artist), "id": ""}],
|
||||||
"duration_ms": duration_map.get(s.asin, 0),
|
"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,
|
"disc_number": s.disc_number,
|
||||||
"release_date": s.date or "",
|
"release_date": s.date or "",
|
||||||
"isrc": s.isrc,
|
"isrc": s.isrc,
|
||||||
}
|
}
|
||||||
for s in streams
|
for idx, s in enumerate(streams)
|
||||||
]
|
]
|
||||||
return {"items": items, "total": len(items), "limit": 50, "next": None}
|
return {"items": items, "total": len(items), "limit": 50, "next": None}
|
||||||
|
|
||||||
|
|
@ -547,7 +547,7 @@ class AmazonClient:
|
||||||
_rate_limit()
|
_rate_limit()
|
||||||
search_name = _unslugify(artist_name)
|
search_name = _unslugify(artist_name)
|
||||||
try:
|
try:
|
||||||
items = self.search_raw(search_name, types="track,album")
|
items = self.search_raw(search_name, types="track")
|
||||||
except AmazonClientError:
|
except AmazonClientError:
|
||||||
return None
|
return None
|
||||||
name_lower = search_name.lower()
|
name_lower = search_name.lower()
|
||||||
|
|
@ -577,7 +577,7 @@ class AmazonClient:
|
||||||
_rate_limit()
|
_rate_limit()
|
||||||
search_name = _unslugify(artist_name)
|
search_name = _unslugify(artist_name)
|
||||||
try:
|
try:
|
||||||
items = self.search_raw(f"{search_name} album", types="track,album")
|
items = self.search_raw(f"{search_name} album", types="track")
|
||||||
except AmazonClientError:
|
except AmazonClientError:
|
||||||
return []
|
return []
|
||||||
album_candidates: List[tuple] = [] # (Album, asin)
|
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)."""
|
"""Return an album cover as artist image stand-in (T2Tunes has no artist images)."""
|
||||||
search_name = _unslugify(artist_id)
|
search_name = _unslugify(artist_id)
|
||||||
try:
|
try:
|
||||||
items = self.search_raw(search_name, types="track,album")
|
items = self.search_raw(search_name, types="track")
|
||||||
except AmazonClientError:
|
except AmazonClientError:
|
||||||
return None
|
return None
|
||||||
name_lower = search_name.lower()
|
name_lower = search_name.lower()
|
||||||
|
|
@ -686,20 +686,33 @@ class AmazonClient:
|
||||||
|
|
||||||
def _get_json(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
def _get_json(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
||||||
url = urljoin(f"{self.base_url}/", path.lstrip("/"))
|
url = urljoin(f"{self.base_url}/", path.lstrip("/"))
|
||||||
try:
|
last_exc: Optional[Exception] = None
|
||||||
resp = self.session.get(url, params=params, timeout=self.timeout)
|
for attempt in range(3):
|
||||||
resp.raise_for_status()
|
if attempt:
|
||||||
except requests.HTTPError as exc:
|
time.sleep(1.0 * attempt)
|
||||||
raise AmazonClientError(
|
try:
|
||||||
f"HTTP {exc.response.status_code} for {url}"
|
resp = self.session.get(url, params=params, timeout=self.timeout)
|
||||||
) from exc
|
resp.raise_for_status()
|
||||||
except requests.RequestException as exc:
|
except requests.HTTPError as exc:
|
||||||
raise AmazonClientError(f"Request failed for {url}: {exc}") from exc
|
body = exc.response.text[:500].replace("\n", " ")
|
||||||
try:
|
# T2Tunes returns 400 for transient Amazon-side failures — retry those.
|
||||||
return resp.json()
|
if exc.response.status_code == 400 and "Failed to search" in exc.response.text:
|
||||||
except ValueError as exc:
|
logger.debug("T2Tunes transient 400 on attempt %d, retrying: %s", attempt + 1, url)
|
||||||
preview = resp.text[:200].replace("\n", " ")
|
last_exc = AmazonClientError(
|
||||||
raise AmazonClientError(f"Response not JSON for {url}: {preview!r}") from exc
|
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
|
@staticmethod
|
||||||
def _iter_search_items(response: Any) -> Iterator[T2TunesSearchItem]:
|
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: '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: 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: 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': [
|
'2.5.5': [
|
||||||
{ date: 'May 17, 2026 — 2.5.5 release' },
|
{ date: 'May 17, 2026 — 2.5.5 release' },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue