mirror of https://github.com/nathom/streamrip.git
300 lines
8.1 KiB
Python
300 lines
8.1 KiB
Python
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 clean(self.name)
|
|
|
|
def preview(self) -> str:
|
|
return f"{self.num_albums} Albums\n\nID: {self.id}"
|
|
|
|
@classmethod
|
|
def from_item(cls, item: dict):
|
|
id = str(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:
|
|
# This char breaks the menu for some reason
|
|
return f"{clean(self.name)} by {clean(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 = str(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("streamStartDate")
|
|
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"{clean(self.name)} by {clean(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 = str(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 item.get("numberOfTracks", 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("releaseDate")
|
|
or item.get("display_date")
|
|
or item.get("date")
|
|
or item.get("year")
|
|
or "Unknown"
|
|
)
|
|
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 = str(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:
|
|
name = clean(self.name)
|
|
creator = clean(self.creator)
|
|
return f"{name} by {creator}"
|
|
|
|
def preview(self) -> str:
|
|
desc = clean(self.description, trunc=False)
|
|
wrapped = "\n".join(
|
|
textwrap.wrap(desc, 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.get("id") or item.get("uuid") or "Unknown"
|
|
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 item.get("numberOfTracks")
|
|
or len(item.get("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))
|
|
elif source == "tidal":
|
|
for item in page["items"]:
|
|
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()
|
|
|
|
def as_list(self, source: str) -> list[dict[str, str]]:
|
|
return [
|
|
{
|
|
"source": source,
|
|
"media_type": i.media_type(),
|
|
"id": i.id,
|
|
"desc": i.summarize(),
|
|
}
|
|
for i in self.results
|
|
]
|
|
|
|
|
|
def clean(s: str, trunc=True) -> str:
|
|
s = s.replace("|", "").replace("\n", "")
|
|
if trunc:
|
|
max_chars = 50
|
|
return s[:max_chars]
|
|
return s
|