diff --git a/pyproject.toml b/pyproject.toml index e5e8b20..93294d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "streamrip" -version = "0.6.4" +version = "0.6.5" description = "A fast, all-in-one music ripper for Qobuz, Deezer, Tidal, and SoundCloud" authors = ["nathom "] license = "GPL-3.0-only" diff --git a/streamrip/__init__.py b/streamrip/__init__.py index c87e21d..c02b59f 100644 --- a/streamrip/__init__.py +++ b/streamrip/__init__.py @@ -1,3 +1,3 @@ """streamrip: the all in one music downloader.""" -__version__ = "0.6.4" +__version__ = "0.6.5" diff --git a/streamrip/bases.py b/streamrip/bases.py index c3773f5..845559d 100644 --- a/streamrip/bases.py +++ b/streamrip/bases.py @@ -842,7 +842,6 @@ class Tracklist(list): if kwargs.get("concurrent_downloads", True): # Tidal errors out with unlimited concurrency - # max_workers = 15 if self.client.source == "tidal" else 90 with concurrent.futures.ThreadPoolExecutor(15) as executor: futures = [executor.submit(target, item, **kwargs) for item in self] try: diff --git a/streamrip/core.py b/streamrip/core.py index 1bc03b7..e13a69a 100644 --- a/streamrip/core.py +++ b/streamrip/core.py @@ -249,7 +249,7 @@ class MusicDL(list): if not (isinstance(item, Tracklist) and item.loaded): logger.debug("Loading metadata") try: - item.load_meta() + item.load_meta(**arguments) except NonStreamable: click.secho(f"{item!s} is not available, skipping.", fg="red") continue diff --git a/streamrip/metadata.py b/streamrip/metadata.py index 194e63f..1153283 100644 --- a/streamrip/metadata.py +++ b/streamrip/metadata.py @@ -19,7 +19,7 @@ from .constants import ( TRACK_KEYS, ) from .exceptions import InvalidContainerError, InvalidSourceError -from .utils import get_quality_id, safe_get, tidal_cover_url +from .utils import get_quality_id, safe_get, tidal_cover_url, get_cover_urls logger = logging.getLogger("streamrip") @@ -151,8 +151,7 @@ class TrackMetadata: # Non-embedded information self.version = resp.get("version") - self.cover_urls = OrderedDict(resp["image"]) - self.cover_urls["original"] = self.cover_urls["large"].replace("600", "org") + self.cover_urls = get_cover_urls(resp, self.__source) self.streamable = resp.get("streamable", False) self.bit_depth = resp.get("maximum_bit_depth") self.sampling_rate = resp.get("maximum_sampling_rate") @@ -177,13 +176,7 @@ class TrackMetadata: # non-embedded self.explicit = resp.get("explicit", False) # 80, 160, 320, 640, 1280 - uuid = resp.get("cover") - self.cover_urls = OrderedDict( - { - sk: tidal_cover_url(uuid, size) - for sk, size in zip(COVER_SIZES, (160, 320, 640, 1280)) - } - ) + self.cover_urls = get_cover_urls(resp, self.__source) self.streamable = resp.get("allowStreaming", False) if q := resp.get("audioQuality"): # for album entries in single tracks @@ -205,15 +198,7 @@ class TrackMetadata: self.explicit = bool(resp.get("parental_warning")) self.quality = 2 self.bit_depth = 16 - self.cover_urls = OrderedDict( - { - sk: resp.get(rk) # size key, resp key - for sk, rk in zip( - COVER_SIZES, - ("cover", "cover_medium", "cover_large", "cover_xl"), - ) - } - ) + self.cover_urls = get_cover_urls(resp, self.__source) self.sampling_rate = 44100 self.streamable = True diff --git a/streamrip/tracklists.py b/streamrip/tracklists.py index d86d885..ee18b71 100644 --- a/streamrip/tracklists.py +++ b/streamrip/tracklists.py @@ -19,6 +19,7 @@ from .exceptions import InvalidSourceError, NonStreamable from .metadata import TrackMetadata from .utils import ( clean_format, + get_cover_urls, get_container, get_stats_from_quality, safe_get, @@ -68,7 +69,7 @@ class Album(Tracklist): self.loaded = False self.downloaded = False - def load_meta(self): + def load_meta(self, **kwargs): """Load detailed metadata from API using the id.""" assert hasattr(self, "id"), "id must be set to load metadata" resp = self.client.get(self.id, media_type="album") @@ -220,7 +221,7 @@ class Album(Tracklist): This uses a classmethod to convert an item into a Track object, which stores the metadata inside a TrackMetadata object. """ - logging.debug(f"Loading {self.tracktotal} tracks to album") + logging.debug("Loading %d tracks to album", self.tracktotal) for track in _get_tracklist(resp, self.client.source): if track.get("type") == "Music Video": self.append(Video.from_album_meta(track, self.client)) @@ -238,7 +239,13 @@ class Album(Tracklist): """ fmt = {key: self.get(key) for key in ALBUM_KEYS} - stats = get_stats_from_quality(self.quality) + stats = tuple( + min(bd, sr) + for bd, sr in zip( + (self.meta.bit_depth, self.meta.sampling_rate), + get_stats_from_quality(self.quality), + ) + ) # The quality chosen is not the maximum available quality if stats != (fmt.get("sampling_rate"), fmt.get("bit_depth")): @@ -338,6 +345,7 @@ class Playlist(Tracklist): :type album_id: Union[str, int] :param kwargs: """ + self.name: str self.client = client for k, v in kwargs.items(): @@ -373,7 +381,7 @@ class Playlist(Tracklist): self._load_tracks(**kwargs) self.loaded = True - def _load_tracks(self, new_tracknumbers: bool = True): + def _load_tracks(self, new_tracknumbers: bool = True, **kwargs): """Parse the tracklist returned by the API. :param new_tracknumbers: replace tracknumber tag with playlist position @@ -386,9 +394,6 @@ class Playlist(Tracklist): tracklist = self.meta["tracks"]["items"] - def gen_cover(track): - return track["album"]["image"]["small"] - def meta_args(track): return {"track": track, "album": track["album"]} @@ -399,10 +404,6 @@ class Playlist(Tracklist): tracklist = self.meta["tracks"] - def gen_cover(track): - cover_url = tidal_cover_url(track["album"]["cover"], 640) - return cover_url - def meta_args(track): return { "track": track, @@ -416,18 +417,12 @@ class Playlist(Tracklist): tracklist = self.meta["tracks"] - def gen_cover(track): - return track["album"]["cover_medium"] - elif self.client.source == "soundcloud": self.name = self.meta["title"] # self.image = self.meta.get("artwork_url").replace("large", "t500x500") self.creator = self.meta["user"]["username"] tracklist = self.meta["tracks"] - def gen_cover(track): - return track["artwork_url"].replace("large", "t500x500") - else: raise NotImplementedError @@ -443,13 +438,16 @@ class Playlist(Tracklist): # tracknumber tags might cause conflicts if the playlist files are # inside of a library folder meta = TrackMetadata(track=track, source=self.client.source) + cover_url = get_cover_urls(track["album"], self.client.source)[ + kwargs.get("embed_cover_size", "large") + ] self.append( Track( self.client, id=track.get("id"), meta=meta, - cover_url=gen_cover(track), + cover_url=cover_url, part_of_tracklist=True, ) ) @@ -484,7 +482,7 @@ class Playlist(Tracklist): if self.downloaded and self.client.source != "deezer": item.tag(embed_cover=kwargs.get("embed_cover", True)) - if playlist_to_album and self.client.source == "deezer": + if self.downloaded and playlist_to_album and self.client.source == "deezer": # Because Deezer tracks come pre-tagged, the `set_playlist_to_album` # option is never set. Here, we manually do this from mutagen.flac import FLAC @@ -584,7 +582,7 @@ class Artist(Tracklist): self.loaded = False - def load_meta(self): + def load_meta(self, **kwargs): """Send an API call to get album info based on id.""" self.meta = self.client.get(self.id, media_type="artist") self._load_albums() @@ -857,7 +855,7 @@ class Artist(Tracklist): class Label(Artist): """Represents a downloadable Label.""" - def load_meta(self): + def load_meta(self, **kwargs): """Load metadata given an id.""" assert self.client.source == "qobuz", "Label source must be qobuz" diff --git a/streamrip/utils.py b/streamrip/utils.py index 0571c92..adf44cb 100644 --- a/streamrip/utils.py +++ b/streamrip/utils.py @@ -8,6 +8,7 @@ import os import re from string import Formatter from typing import Dict, Hashable, Optional, Tuple, Union +from collections import OrderedDict import click import requests @@ -15,7 +16,7 @@ from pathvalidate import sanitize_filename from requests.packages import urllib3 from tqdm import tqdm -from .constants import AGENT, TIDAL_COVER_URL +from .constants import AGENT, TIDAL_COVER_URL, COVER_SIZES from .exceptions import InvalidQuality, InvalidSourceError, NonStreamable urllib3.disable_warnings() @@ -382,3 +383,32 @@ def get_container(quality: int, source: str) -> str: return "AAC" return "MP3" + + +def get_cover_urls(resp: dict, source: str) -> dict: + if source == "qobuz": + cover_urls = OrderedDict(resp["image"]) + cover_urls["original"] = cover_urls["large"].replace("600", "org") + return cover_urls + + if source == "tidal": + uuid = resp["cover"] + return OrderedDict( + { + sk: tidal_cover_url(uuid, size) + for sk, size in zip(COVER_SIZES, (160, 320, 640, 1280)) + } + ) + + if source == "deezer": + return OrderedDict( + { + sk: resp.get(rk) # size key, resp key + for sk, rk in zip( + COVER_SIZES, + ("cover", "cover_medium", "cover_large", "cover_xl"), + ) + } + ) + + raise InvalidSourceError(source)