Files
2026-04-03 15:53:00 +01:00

293 lines
8.4 KiB
Python

import msgspec
from core.exceptions import ExternalServiceError
class LastFmToken(msgspec.Struct):
token: str
class LastFmSession(msgspec.Struct):
name: str
key: str
subscriber: int = 0
class LastFmTag(msgspec.Struct):
name: str
url: str = ""
class LastFmArtist(msgspec.Struct):
name: str
mbid: str | None = None
playcount: int = 0
listeners: int = 0
url: str = ""
class LastFmAlbum(msgspec.Struct):
name: str
artist_name: str
mbid: str | None = None
playcount: int = 0
listeners: int = 0
url: str = ""
image_url: str = ""
class LastFmTrack(msgspec.Struct):
name: str
artist_name: str
mbid: str | None = None
playcount: int = 0
listeners: int = 0
url: str = ""
class LastFmSimilarArtist(msgspec.Struct):
name: str
mbid: str | None = None
match: float = 0.0
url: str = ""
class LastFmArtistInfo(msgspec.Struct):
name: str
mbid: str | None = None
listeners: int = 0
playcount: int = 0
url: str = ""
bio_summary: str = ""
tags: list[LastFmTag] | None = None
similar: list[LastFmSimilarArtist] | None = None
class LastFmAlbumTrack(msgspec.Struct):
name: str
duration: int = 0
rank: int = 0
url: str = ""
class LastFmAlbumInfo(msgspec.Struct):
name: str
artist_name: str
mbid: str | None = None
listeners: int = 0
playcount: int = 0
url: str = ""
image_url: str = ""
summary: str = ""
tags: list[LastFmTag] | None = None
tracks: list[LastFmAlbumTrack] | None = None
class LastFmRecentTrack(msgspec.Struct):
track_name: str
artist_name: str
album_name: str = ""
artist_mbid: str | None = None
album_mbid: str | None = None
timestamp: int = 0
now_playing: bool = False
image_url: str = ""
class LastFmLovedTrack(msgspec.Struct):
track_name: str
artist_name: str
album_name: str = ""
track_mbid: str | None = None
artist_mbid: str | None = None
url: str = ""
image_url: str = ""
ALLOWED_LASTFM_PERIOD = [
"overall", "7day", "1month", "3month", "6month", "12month",
]
def parse_weekly_album_chart_item(item: dict) -> "LastFmAlbum":
artist = item.get("artist", {})
artist_name = artist.get("#text", "") if isinstance(artist, dict) else str(artist)
return LastFmAlbum(
name=item.get("name", ""),
artist_name=artist_name,
mbid=item.get("mbid") or None,
playcount=_safe_int(item.get("playcount")),
url=item.get("url", ""),
image_url=_extract_image(item.get("image")),
)
def parse_token(data: dict) -> LastFmToken:
token_value = data.get("token")
if not token_value:
raise ExternalServiceError("Last.fm auth.getToken response missing 'token'")
return LastFmToken(token=token_value)
def parse_session(data: dict) -> LastFmSession:
session_data = data.get("session", data)
name = session_data.get("name")
key = session_data.get("key")
if not name or not key:
raise ExternalServiceError("Last.fm auth.getSession response missing 'name' or 'key'")
return LastFmSession(
name=name,
key=key,
subscriber=int(session_data.get("subscriber", 0)),
)
def _extract_image(images: list[dict] | None, size: str = "extralarge") -> str:
if not images:
return ""
for img in images:
if img.get("size") == size:
return img.get("#text", "")
return images[-1].get("#text", "") if images else ""
def _safe_int(value: str | int | None, default: int = 0) -> int:
if value is None:
return default
try:
return int(value)
except (ValueError, TypeError):
return default
def _safe_float(value: str | float | None, default: float = 0.0) -> float:
if value is None:
return default
try:
return float(value)
except (ValueError, TypeError):
return default
def parse_top_artist(item: dict) -> LastFmArtist:
return LastFmArtist(
name=item.get("name", ""),
mbid=item.get("mbid") or None,
playcount=_safe_int(item.get("playcount")),
listeners=_safe_int(item.get("listeners")),
url=item.get("url", ""),
)
def parse_top_album(item: dict) -> LastFmAlbum:
artist = item.get("artist", {})
artist_name = artist.get("name", "") if isinstance(artist, dict) else str(artist)
return LastFmAlbum(
name=item.get("name", ""),
artist_name=artist_name,
mbid=item.get("mbid") or None,
playcount=_safe_int(item.get("playcount")),
listeners=_safe_int(item.get("listeners")),
url=item.get("url", ""),
image_url=_extract_image(item.get("image")),
)
def parse_top_track(item: dict) -> LastFmTrack:
artist = item.get("artist", {})
artist_name = artist.get("name", "") if isinstance(artist, dict) else str(artist)
return LastFmTrack(
name=item.get("name", ""),
artist_name=artist_name,
mbid=item.get("mbid") or None,
playcount=_safe_int(item.get("playcount")),
listeners=_safe_int(item.get("listeners")),
url=item.get("url", ""),
)
def parse_similar_artist(item: dict) -> LastFmSimilarArtist:
return LastFmSimilarArtist(
name=item.get("name", ""),
mbid=item.get("mbid") or None,
match=_safe_float(item.get("match")),
url=item.get("url", ""),
)
def parse_artist_info(data: dict) -> LastFmArtistInfo:
artist = data.get("artist", {})
stats = artist.get("stats", {})
tags_data = artist.get("tags", {}).get("tag", [])
similar_data = artist.get("similar", {}).get("artist", [])
bio = artist.get("bio", {})
return LastFmArtistInfo(
name=artist.get("name", ""),
mbid=artist.get("mbid") or None,
listeners=_safe_int(stats.get("listeners")),
playcount=_safe_int(stats.get("playcount")),
url=artist.get("url", ""),
bio_summary=bio.get("summary", ""),
tags=[LastFmTag(name=t.get("name", ""), url=t.get("url", "")) for t in tags_data],
similar=[parse_similar_artist(s) for s in similar_data],
)
def parse_album_info(data: dict) -> LastFmAlbumInfo:
album = data.get("album", {})
tags_data = album.get("tags", {}).get("tag", [])
tracks_data = album.get("tracks", {}).get("track", [])
wiki = album.get("wiki", {})
tracks = [
LastFmAlbumTrack(
name=t.get("name", ""),
duration=_safe_int(t.get("duration")),
rank=_safe_int(t.get("@attr", {}).get("rank")),
url=t.get("url", ""),
)
for t in tracks_data
] if tracks_data else None
return LastFmAlbumInfo(
name=album.get("name", ""),
artist_name=album.get("artist", ""),
mbid=album.get("mbid") or None,
listeners=_safe_int(album.get("listeners")),
playcount=_safe_int(album.get("playcount")),
url=album.get("url", ""),
image_url=_extract_image(album.get("image")),
summary=wiki.get("summary", ""),
tags=[LastFmTag(name=t.get("name", ""), url=t.get("url", "")) for t in tags_data],
tracks=tracks,
)
def parse_recent_track(item: dict) -> LastFmRecentTrack:
artist = item.get("artist", {})
album = item.get("album", {})
date = item.get("date", {})
attr = item.get("@attr", {})
return LastFmRecentTrack(
track_name=item.get("name", ""),
artist_name=artist.get("#text", "") if isinstance(artist, dict) else str(artist),
album_name=album.get("#text", "") if isinstance(album, dict) else str(album),
artist_mbid=artist.get("mbid") or None if isinstance(artist, dict) else None,
album_mbid=album.get("mbid") or None if isinstance(album, dict) else None,
timestamp=_safe_int(date.get("uts")) if isinstance(date, dict) else 0,
now_playing=attr.get("nowplaying") == "true",
image_url=_extract_image(item.get("image")),
)
def parse_loved_track(item: dict) -> LastFmLovedTrack:
artist = item.get("artist", {})
album = item.get("album", {})
return LastFmLovedTrack(
track_name=item.get("name", ""),
artist_name=artist.get("name", "") if isinstance(artist, dict) else str(artist),
album_name=album.get("#text", "") if isinstance(album, dict) else str(album),
track_mbid=item.get("mbid") or None,
artist_mbid=artist.get("mbid") if isinstance(artist, dict) else None,
url=item.get("url", ""),
image_url=_extract_image(item.get("image")),
)