mirror of
https://github.com/nathom/streamrip.git
synced 2025-05-18 09:04:51 -04:00
Merge branch 'dev'
This commit is contained in:
commit
70a0928db5
7 changed files with 57 additions and 45 deletions
|
@ -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 <nathanthomas707@gmail.com>"]
|
||||
license = "GPL-3.0-only"
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
"""streamrip: the all in one music downloader."""
|
||||
|
||||
__version__ = "0.6.4"
|
||||
__version__ = "0.6.5"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue