Merge branch 'dev'

This commit is contained in:
nathom 2021-06-29 10:03:20 -07:00
commit 70a0928db5
7 changed files with 57 additions and 45 deletions

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "streamrip" name = "streamrip"
version = "0.6.4" version = "0.6.5"
description = "A fast, all-in-one music ripper for Qobuz, Deezer, Tidal, and SoundCloud" description = "A fast, all-in-one music ripper for Qobuz, Deezer, Tidal, and SoundCloud"
authors = ["nathom <nathanthomas707@gmail.com>"] authors = ["nathom <nathanthomas707@gmail.com>"]
license = "GPL-3.0-only" license = "GPL-3.0-only"

View file

@ -1,3 +1,3 @@
"""streamrip: the all in one music downloader.""" """streamrip: the all in one music downloader."""
__version__ = "0.6.4" __version__ = "0.6.5"

View file

@ -842,7 +842,6 @@ class Tracklist(list):
if kwargs.get("concurrent_downloads", True): if kwargs.get("concurrent_downloads", True):
# Tidal errors out with unlimited concurrency # Tidal errors out with unlimited concurrency
# max_workers = 15 if self.client.source == "tidal" else 90
with concurrent.futures.ThreadPoolExecutor(15) as executor: with concurrent.futures.ThreadPoolExecutor(15) as executor:
futures = [executor.submit(target, item, **kwargs) for item in self] futures = [executor.submit(target, item, **kwargs) for item in self]
try: try:

View file

@ -249,7 +249,7 @@ class MusicDL(list):
if not (isinstance(item, Tracklist) and item.loaded): if not (isinstance(item, Tracklist) and item.loaded):
logger.debug("Loading metadata") logger.debug("Loading metadata")
try: try:
item.load_meta() item.load_meta(**arguments)
except NonStreamable: except NonStreamable:
click.secho(f"{item!s} is not available, skipping.", fg="red") click.secho(f"{item!s} is not available, skipping.", fg="red")
continue continue

View file

@ -19,7 +19,7 @@ from .constants import (
TRACK_KEYS, TRACK_KEYS,
) )
from .exceptions import InvalidContainerError, InvalidSourceError 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") logger = logging.getLogger("streamrip")
@ -151,8 +151,7 @@ class TrackMetadata:
# Non-embedded information # Non-embedded information
self.version = resp.get("version") self.version = resp.get("version")
self.cover_urls = OrderedDict(resp["image"]) self.cover_urls = get_cover_urls(resp, self.__source)
self.cover_urls["original"] = self.cover_urls["large"].replace("600", "org")
self.streamable = resp.get("streamable", False) self.streamable = resp.get("streamable", False)
self.bit_depth = resp.get("maximum_bit_depth") self.bit_depth = resp.get("maximum_bit_depth")
self.sampling_rate = resp.get("maximum_sampling_rate") self.sampling_rate = resp.get("maximum_sampling_rate")
@ -177,13 +176,7 @@ class TrackMetadata:
# non-embedded # non-embedded
self.explicit = resp.get("explicit", False) self.explicit = resp.get("explicit", False)
# 80, 160, 320, 640, 1280 # 80, 160, 320, 640, 1280
uuid = resp.get("cover") self.cover_urls = get_cover_urls(resp, self.__source)
self.cover_urls = OrderedDict(
{
sk: tidal_cover_url(uuid, size)
for sk, size in zip(COVER_SIZES, (160, 320, 640, 1280))
}
)
self.streamable = resp.get("allowStreaming", False) self.streamable = resp.get("allowStreaming", False)
if q := resp.get("audioQuality"): # for album entries in single tracks 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.explicit = bool(resp.get("parental_warning"))
self.quality = 2 self.quality = 2
self.bit_depth = 16 self.bit_depth = 16
self.cover_urls = OrderedDict( self.cover_urls = get_cover_urls(resp, self.__source)
{
sk: resp.get(rk) # size key, resp key
for sk, rk in zip(
COVER_SIZES,
("cover", "cover_medium", "cover_large", "cover_xl"),
)
}
)
self.sampling_rate = 44100 self.sampling_rate = 44100
self.streamable = True self.streamable = True

View file

@ -19,6 +19,7 @@ from .exceptions import InvalidSourceError, NonStreamable
from .metadata import TrackMetadata from .metadata import TrackMetadata
from .utils import ( from .utils import (
clean_format, clean_format,
get_cover_urls,
get_container, get_container,
get_stats_from_quality, get_stats_from_quality,
safe_get, safe_get,
@ -68,7 +69,7 @@ class Album(Tracklist):
self.loaded = False self.loaded = False
self.downloaded = False self.downloaded = False
def load_meta(self): def load_meta(self, **kwargs):
"""Load detailed metadata from API using the id.""" """Load detailed metadata from API using the id."""
assert hasattr(self, "id"), "id must be set to load metadata" assert hasattr(self, "id"), "id must be set to load metadata"
resp = self.client.get(self.id, media_type="album") 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 This uses a classmethod to convert an item into a Track object, which
stores the metadata inside a TrackMetadata object. 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): for track in _get_tracklist(resp, self.client.source):
if track.get("type") == "Music Video": if track.get("type") == "Music Video":
self.append(Video.from_album_meta(track, self.client)) 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} 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 # The quality chosen is not the maximum available quality
if stats != (fmt.get("sampling_rate"), fmt.get("bit_depth")): if stats != (fmt.get("sampling_rate"), fmt.get("bit_depth")):
@ -338,6 +345,7 @@ class Playlist(Tracklist):
:type album_id: Union[str, int] :type album_id: Union[str, int]
:param kwargs: :param kwargs:
""" """
self.name: str
self.client = client self.client = client
for k, v in kwargs.items(): for k, v in kwargs.items():
@ -373,7 +381,7 @@ class Playlist(Tracklist):
self._load_tracks(**kwargs) self._load_tracks(**kwargs)
self.loaded = True 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. """Parse the tracklist returned by the API.
:param new_tracknumbers: replace tracknumber tag with playlist position :param new_tracknumbers: replace tracknumber tag with playlist position
@ -386,9 +394,6 @@ class Playlist(Tracklist):
tracklist = self.meta["tracks"]["items"] tracklist = self.meta["tracks"]["items"]
def gen_cover(track):
return track["album"]["image"]["small"]
def meta_args(track): def meta_args(track):
return {"track": track, "album": track["album"]} return {"track": track, "album": track["album"]}
@ -399,10 +404,6 @@ class Playlist(Tracklist):
tracklist = self.meta["tracks"] tracklist = self.meta["tracks"]
def gen_cover(track):
cover_url = tidal_cover_url(track["album"]["cover"], 640)
return cover_url
def meta_args(track): def meta_args(track):
return { return {
"track": track, "track": track,
@ -416,18 +417,12 @@ class Playlist(Tracklist):
tracklist = self.meta["tracks"] tracklist = self.meta["tracks"]
def gen_cover(track):
return track["album"]["cover_medium"]
elif self.client.source == "soundcloud": elif self.client.source == "soundcloud":
self.name = self.meta["title"] self.name = self.meta["title"]
# self.image = self.meta.get("artwork_url").replace("large", "t500x500") # self.image = self.meta.get("artwork_url").replace("large", "t500x500")
self.creator = self.meta["user"]["username"] self.creator = self.meta["user"]["username"]
tracklist = self.meta["tracks"] tracklist = self.meta["tracks"]
def gen_cover(track):
return track["artwork_url"].replace("large", "t500x500")
else: else:
raise NotImplementedError raise NotImplementedError
@ -443,13 +438,16 @@ class Playlist(Tracklist):
# tracknumber tags might cause conflicts if the playlist files are # tracknumber tags might cause conflicts if the playlist files are
# inside of a library folder # inside of a library folder
meta = TrackMetadata(track=track, source=self.client.source) 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( self.append(
Track( Track(
self.client, self.client,
id=track.get("id"), id=track.get("id"),
meta=meta, meta=meta,
cover_url=gen_cover(track), cover_url=cover_url,
part_of_tracklist=True, part_of_tracklist=True,
) )
) )
@ -484,7 +482,7 @@ class Playlist(Tracklist):
if self.downloaded and self.client.source != "deezer": if self.downloaded and self.client.source != "deezer":
item.tag(embed_cover=kwargs.get("embed_cover", True)) 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` # Because Deezer tracks come pre-tagged, the `set_playlist_to_album`
# option is never set. Here, we manually do this # option is never set. Here, we manually do this
from mutagen.flac import FLAC from mutagen.flac import FLAC
@ -584,7 +582,7 @@ class Artist(Tracklist):
self.loaded = False self.loaded = False
def load_meta(self): def load_meta(self, **kwargs):
"""Send an API call to get album info based on id.""" """Send an API call to get album info based on id."""
self.meta = self.client.get(self.id, media_type="artist") self.meta = self.client.get(self.id, media_type="artist")
self._load_albums() self._load_albums()
@ -857,7 +855,7 @@ class Artist(Tracklist):
class Label(Artist): class Label(Artist):
"""Represents a downloadable Label.""" """Represents a downloadable Label."""
def load_meta(self): def load_meta(self, **kwargs):
"""Load metadata given an id.""" """Load metadata given an id."""
assert self.client.source == "qobuz", "Label source must be qobuz" assert self.client.source == "qobuz", "Label source must be qobuz"

View file

@ -8,6 +8,7 @@ import os
import re import re
from string import Formatter from string import Formatter
from typing import Dict, Hashable, Optional, Tuple, Union from typing import Dict, Hashable, Optional, Tuple, Union
from collections import OrderedDict
import click import click
import requests import requests
@ -15,7 +16,7 @@ from pathvalidate import sanitize_filename
from requests.packages import urllib3 from requests.packages import urllib3
from tqdm import tqdm 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 from .exceptions import InvalidQuality, InvalidSourceError, NonStreamable
urllib3.disable_warnings() urllib3.disable_warnings()
@ -382,3 +383,32 @@ def get_container(quality: int, source: str) -> str:
return "AAC" return "AAC"
return "MP3" 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)