import os import re import textwrap from abc import ABC, abstractmethod from dataclasses import dataclass class Summary(ABC): id: str @abstractmethod def summarize(self) -> str: pass @abstractmethod def preview(self) -> str: pass @classmethod @abstractmethod def from_item(cls, item: dict) -> str: pass @abstractmethod def media_type(self) -> str: pass def __str__(self): return self.summarize() @dataclass(slots=True) class ArtistSummary(Summary): id: str name: str num_albums: str def media_type(self): return "artist" def summarize(self) -> str: return self.name def preview(self) -> str: return f"{self.num_albums} Albums\n\nID: {self.id}" @classmethod def from_item(cls, item: dict): id = item["id"] name = ( item.get("name") or item.get("performer", {}).get("name") or item.get("artist") or item.get("artist", {}).get("name") or ( item.get("publisher_metadata") and item["publisher_metadata"].get("artist") ) or "Unknown" ) num_albums = item.get("albums_count") or "Unknown" return cls(id, name, num_albums) @dataclass(slots=True) class TrackSummary(Summary): id: str name: str artist: str date_released: str | None def media_type(self): return "track" def summarize(self) -> str: return f"{self.name} by {self.artist}" def preview(self) -> str: return f"Released on:\n{self.date_released}\n\nID: {self.id}" @classmethod def from_item(cls, item: dict): id = item["id"] name = item.get("title") or item.get("name") or "Unknown" artist = ( item.get("performer", {}).get("name") or item.get("artist") or item.get("artist", {}).get("name") or ( item.get("publisher_metadata") and item["publisher_metadata"].get("artist") ) or "Unknown" ) if isinstance(artist, dict) and "name" in artist: artist = artist["name"] date_released = ( item.get("release_date") or item.get("album", {}).get("release_date_original") or item.get("display_date") or item.get("date") or item.get("year") or "Unknown" ) return cls(id, name.strip(), artist, date_released) # type: ignore @dataclass(slots=True) class AlbumSummary(Summary): id: str name: str artist: str num_tracks: str date_released: str | None def media_type(self): return "album" def summarize(self) -> str: return f"{self.name} by {self.artist}" def preview(self) -> str: return f"Date released:\n{self.date_released}\n\n{self.num_tracks} Tracks\n\nID: {self.id}" @classmethod def from_item(cls, item: dict): id = item["id"] name = item.get("title") or "Unknown Title" artist = ( item.get("performer", {}).get("name") or item.get("artist", {}).get("name") or item.get("artist") or ( item.get("publisher_metadata") and item["publisher_metadata"].get("artist") ) or "Unknown" ) num_tracks = item.get("tracks_count", 0) or len( item.get("tracks", []) or item.get("items", []), ) date_released = ( item.get("release_date_original") or item.get("release_date") or item.get("display_date") or item.get("date") or item.get("year") or "Unknown" ) # raise Exception(item) return cls(id, name, artist, str(num_tracks), date_released) @dataclass(slots=True) class LabelSummary(Summary): id: str name: str def media_type(self): return "label" def summarize(self) -> str: return str(self) def preview(self) -> str: return str(self) @classmethod def from_item(cls, item: dict): id = item["id"] name = item["name"] return cls(id, name) @dataclass(slots=True) class PlaylistSummary(Summary): id: str name: str creator: str num_tracks: int description: str def summarize(self) -> str: return f"{self.name} by {self.creator}" def preview(self) -> str: wrapped = "\n".join( textwrap.wrap(self.description, os.get_terminal_size().columns - 4 or 70), ) return f"{self.num_tracks} tracks\n\nDescription:\n{wrapped}\n\nID: {self.id}" def media_type(self): return "playlist" @classmethod def from_item(cls, item: dict): id = item["id"] name = item.get("name") or item.get("title") or "Unknown" creator = ( (item.get("publisher_metadata") and item["publisher_metadata"]["artist"]) or item.get("owner", {}).get("name") or item.get("user", {}).get("username") or item.get("user", {}).get("name") or "Unknown" ) num_tracks = item.get("tracks_count") or item.get("nb_tracks") or -1 description = item.get("description") or "No description" return cls(id, name, creator, num_tracks, description) @dataclass(slots=True) class SearchResults: results: list[Summary] @classmethod def from_pages(cls, source: str, media_type: str, pages: list[dict]): if media_type == "track": summary_type = TrackSummary elif media_type == "album": summary_type = AlbumSummary elif media_type == "label": summary_type = LabelSummary elif media_type == "artist": summary_type = ArtistSummary elif media_type == "playlist": summary_type = PlaylistSummary else: raise Exception(f"invalid media type {media_type}") results = [] for page in pages: if source == "soundcloud": items = page["collection"] for item in items: results.append(summary_type.from_item(item)) elif source == "qobuz": key = media_type + "s" for item in page[key]["items"]: results.append(summary_type.from_item(item)) elif source == "deezer": for item in page["data"]: results.append(summary_type.from_item(item)) else: raise NotImplementedError return cls(results) def summaries(self) -> list[str]: return [f"{i+1}. {r.summarize()}" for i, r in enumerate(self.results)] def get_choices(self, inds: tuple[int, ...] | int): if isinstance(inds, int): inds = (inds,) return [self.results[i] for i in inds] def preview(self, s: str) -> str: ind = re.match(r"^\d+", s) assert ind is not None i = int(ind.group(0)) return self.results[i - 1].preview()