fix: look tags up by artist name rather than by MusicBrainz id
Build and publish container / build (pull_request) Successful in 2m53s
Build and publish container / build (pull_request) Successful in 2m53s
Tag lookups asked by MusicBrainz id whenever Lidarr had one, which is always. The reasoning was that an id cannot be ambiguous the way a name can. In practice Last.fm's mbid index is stale and partial, and it answered "the artist you supplied could not be found" for Devo, Escape the Fate, Blasterjaxx, Frank Carter & the Rattlesnakes and a long tail of others -- artists whose Last.fm pages plainly exist and carry precisely the tags the moods are built from. Devo is tagged new wave, post-punk and 80s; Escape the Fate is tagged post-hardcore, screamo and emocore. Both were skipped. Ask by name first, which is the index Last.fm's own site runs on, and keep the id only as a fallback for a name Lidarr spells differently. Failures were also being dropped without recording the attempt, so every one of those artists was re-queried on every subsequent pass, indefinitely. They are now distinguished: an artist that neither key resolves is recorded as fetched with no tags and not asked about again, while a genuine failure -- a rate limit, a bad key -- is deliberately left unrecorded so the next pass retries it. Telling the two apart needed the service's own error number, so LastfmError now carries it.
This commit is contained in:
+50
-10
@@ -266,7 +266,16 @@ WHITESPACE = re.compile(r"\s+")
|
||||
|
||||
|
||||
class LastfmError(Exception):
|
||||
"""A Last.fm request that failed in a way retrying will not fix."""
|
||||
"""A Last.fm request that failed in a way retrying will not fix.
|
||||
|
||||
`code` is the service's own error number where the failure came from the
|
||||
API rather than the transport. Callers need it to tell "this thing does not
|
||||
exist", which is final, from "something went wrong", which is not.
|
||||
"""
|
||||
|
||||
def __init__(self, message, code=None):
|
||||
super().__init__(message)
|
||||
self.code = code
|
||||
|
||||
|
||||
class LidarrError(Exception):
|
||||
@@ -447,7 +456,7 @@ class Lastfm:
|
||||
return payload
|
||||
detail = f"error {code}: {payload.get('message', '')}".strip()
|
||||
if code not in RETRYABLE_ERRORS:
|
||||
raise LastfmError(f"{method}: {detail}")
|
||||
raise LastfmError(f"{method}: {detail}", code=code)
|
||||
self._retry_or_raise(method, attempt, detail, None)
|
||||
|
||||
raise LastfmError(f"{method}: gave up after {self.attempts} attempts")
|
||||
@@ -1323,6 +1332,34 @@ def parse_tags(payload):
|
||||
return pairs
|
||||
|
||||
|
||||
def fetch_tags(client, name, mbid):
|
||||
"""Return an artist's tags, asking by name first.
|
||||
|
||||
By name, not by MusicBrainz id, despite Lidarr having an id for everything.
|
||||
Last.fm's mbid index is stale and partial -- it answers "the artist you
|
||||
supplied could not be found" for Devo, Escape the Fate and a few hundred
|
||||
others whose pages plainly exist -- while the name index is the one its own
|
||||
site runs on. The id is kept only as a fallback for a name Lidarr spells
|
||||
differently.
|
||||
|
||||
Returns an empty list when the artist is genuinely unknown, which the caller
|
||||
records so it is not asked again.
|
||||
"""
|
||||
attempts = [{"artist": name}] if name else []
|
||||
if mbid:
|
||||
attempts.append({"mbid": mbid})
|
||||
|
||||
for query in attempts:
|
||||
try:
|
||||
return parse_tags(client.call("artist.getTopTags", {**query, "autocorrect": 1}))
|
||||
except LastfmError as error:
|
||||
# Error 6 here means "no such artist", which the next key may still
|
||||
# answer. Anything else is a real failure and belongs to the caller.
|
||||
if error.code != 6:
|
||||
raise
|
||||
return []
|
||||
|
||||
|
||||
def sync_tags(client, store, now, limit=0):
|
||||
"""Fetch crowd tags for library artists that have none, or stale ones.
|
||||
|
||||
@@ -1348,20 +1385,23 @@ def sync_tags(client, store, now, limit=0):
|
||||
else:
|
||||
logger.info("fetching tags for %d artists", len(stale))
|
||||
|
||||
tagged = 0
|
||||
resolved = 0
|
||||
unknown = 0
|
||||
for artist in stale:
|
||||
query = {"mbid": artist["mbid"]} if artist["mbid"] else {"artist": artist["name"]}
|
||||
try:
|
||||
payload = client.call("artist.getTopTags", {**query, "autocorrect": 1})
|
||||
pairs = fetch_tags(client, artist["name"], artist["mbid"])
|
||||
except LastfmError as error:
|
||||
# One artist Last.fm cannot answer for is not worth losing the pass.
|
||||
# Something went wrong rather than the artist not existing. Left
|
||||
# unrecorded on purpose, so the next pass tries again.
|
||||
logger.warning("no tags for %s: %s", artist["name"], error)
|
||||
continue
|
||||
store.replace_tags(artist["norm_name"], parse_tags(payload), now)
|
||||
tagged += 1
|
||||
store.replace_tags(artist["norm_name"], pairs, now)
|
||||
resolved += 1
|
||||
if not pairs:
|
||||
unknown += 1
|
||||
|
||||
logger.info("tagged %d artists", tagged)
|
||||
return tagged
|
||||
logger.info("tagged %d artists (%d with nothing to say about them)", resolved, unknown)
|
||||
return resolved
|
||||
|
||||
|
||||
def build_vibe_playlists(store, vibes, mirror_root, library_root, limit, now):
|
||||
|
||||
Reference in New Issue
Block a user