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]
|
[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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue