mirror of https://github.com/nathom/streamrip.git
Merge branch 'dev'
This commit is contained in:
commit
362919068c
Binary file not shown.
51
rip/cli.py
51
rip/cli.py
|
@ -18,6 +18,7 @@ logging.basicConfig(level="WARNING")
|
|||
logger = logging.getLogger("streamrip")
|
||||
|
||||
outdated = False
|
||||
newest_version = __version__
|
||||
|
||||
|
||||
class DownloadCommand(Command):
|
||||
|
@ -43,9 +44,11 @@ class DownloadCommand(Command):
|
|||
|
||||
def handle(self):
|
||||
global outdated
|
||||
global newest_version
|
||||
|
||||
# Use a thread so that it doesn't slow down startup
|
||||
update_check = threading.Thread(target=is_outdated, daemon=True)
|
||||
update_check.start()
|
||||
|
||||
config = Config()
|
||||
path, codec, quality, no_db = clean_options(
|
||||
|
@ -83,16 +86,39 @@ class DownloadCommand(Command):
|
|||
elif not urls and path is None:
|
||||
self.line("<error>Must pass arguments. See </><cmd>rip url -h</cmd>.")
|
||||
|
||||
try:
|
||||
update_check.join()
|
||||
if outdated:
|
||||
self.line(
|
||||
"<info>A new version of streamrip is available! Run</info> "
|
||||
"<cmd>pip3 install streamrip --upgrade to update</cmd>"
|
||||
)
|
||||
except RuntimeError as e:
|
||||
logger.debug("Update check error: %s", e)
|
||||
pass
|
||||
update_check.join()
|
||||
if outdated:
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
self.line(
|
||||
f"<info>Updating streamrip to <title>v{newest_version}</title>...</info>\n"
|
||||
)
|
||||
|
||||
# update in background
|
||||
update_p = subprocess.Popen(
|
||||
["pip3", "install", "streamrip", "--upgrade"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
|
||||
md_header = re.compile(r"#\s+(.+)")
|
||||
bullet_point = re.compile(r"-\s+(.+)")
|
||||
code = re.compile(r"`([^`]+)`")
|
||||
issue_reference = re.compile(r"(#\d+)")
|
||||
|
||||
release_notes = requests.get(
|
||||
"https://api.github.com/repos/nathom/streamrip/releases/latest"
|
||||
).json()["body"]
|
||||
|
||||
release_notes = md_header.sub(r"<header>\1</header>", release_notes)
|
||||
release_notes = bullet_point.sub(r"<options=bold>•</> \1", release_notes)
|
||||
release_notes = code.sub(r"<cmd>\1</cmd>", release_notes)
|
||||
release_notes = issue_reference.sub(r"<options=bold>\1</>", release_notes)
|
||||
|
||||
self.line(release_notes)
|
||||
|
||||
update_p.wait()
|
||||
|
||||
return 0
|
||||
|
||||
|
@ -451,6 +477,7 @@ class Application(BaseApplication):
|
|||
formatter.set_style("path", Style("green", options=["bold"]))
|
||||
formatter.set_style("cmd", Style("magenta"))
|
||||
formatter.set_style("title", Style("yellow", options=["bold"]))
|
||||
formatter.set_style("header", Style("yellow", options=["bold", "underline"]))
|
||||
io.output.set_formatter(formatter)
|
||||
io.error_output.set_formatter(formatter)
|
||||
|
||||
|
@ -494,8 +521,10 @@ def clean_options(*opts):
|
|||
|
||||
def is_outdated():
|
||||
global outdated
|
||||
global newest_version
|
||||
r = requests.get("https://pypi.org/pypi/streamrip/json").json()
|
||||
outdated = r["info"]["version"] != __version__
|
||||
newest_version = r["info"]["version"]
|
||||
outdated = newest_version != __version__
|
||||
|
||||
|
||||
def main():
|
||||
|
|
16
rip/core.py
16
rip/core.py
|
@ -8,6 +8,7 @@ import re
|
|||
from getpass import getpass
|
||||
from hashlib import md5
|
||||
from string import Formatter
|
||||
import threading
|
||||
from typing import Dict, Generator, List, Optional, Tuple, Type, Union
|
||||
|
||||
import requests
|
||||
|
@ -219,7 +220,7 @@ class RipCore(list):
|
|||
"parent_folder": session["downloads"]["folder"],
|
||||
"folder_format": filepaths["folder_format"],
|
||||
"track_format": filepaths["track_format"],
|
||||
"embed_cover": session["artwork"]["embed"],
|
||||
"embed_cover": artwork["embed"],
|
||||
"embed_cover_size": artwork["size"],
|
||||
"keep_hires_cover": artwork["keep_hires_cover"],
|
||||
"set_playlist_to_album": session["metadata"]["set_playlist_to_album"],
|
||||
|
@ -367,7 +368,7 @@ class RipCore(list):
|
|||
if client.source == "deezer" and creds["arl"] == "":
|
||||
if self.config.session["deezer"]["deezloader_warnings"]:
|
||||
secho(
|
||||
"Falling back to Deezloader (max 320kbps MP3). If you have a subscription, run ",
|
||||
"Falling back to Deezloader (unstable). If you have a subscription, run ",
|
||||
nl=False,
|
||||
fg="yellow",
|
||||
)
|
||||
|
@ -385,9 +386,16 @@ class RipCore(list):
|
|||
creds = self.config.creds(client.source)
|
||||
except MissingCredentials:
|
||||
logger.debug("Credentials are missing. Prompting..")
|
||||
get_tokens = threading.Thread(
|
||||
target=client._get_app_id_and_secrets, daemon=True
|
||||
)
|
||||
get_tokens.start()
|
||||
|
||||
self.prompt_creds(client.source)
|
||||
creds = self.config.creds(client.source)
|
||||
|
||||
get_tokens.join()
|
||||
|
||||
if (
|
||||
client.source == "qobuz"
|
||||
and not creds.get("secrets")
|
||||
|
@ -451,7 +459,7 @@ class RipCore(list):
|
|||
for item, url in zip(soundcloud_items, soundcloud_urls)
|
||||
)
|
||||
|
||||
logger.debug(f"Parsed urls: {parsed}")
|
||||
logger.debug("Parsed urls: %s", parsed)
|
||||
|
||||
return parsed
|
||||
|
||||
|
@ -493,7 +501,7 @@ class RipCore(list):
|
|||
# This will match somthing like "Test (Person Remix]" though, so its not perfect
|
||||
banned_words_plain = re.compile(r"(?i)(?:(?:re)?mix|live|karaoke)")
|
||||
banned_words = re.compile(
|
||||
rf"(?i)[\(\[][^\)\]]*?(?:(?:re)?mix|live|karaoke)[^\)\]]*[\]\)]"
|
||||
r"(?i)[\(\[][^\)\]]*?(?:(?:re)?mix|live|karaoke)[^\)\]]*[\]\)]"
|
||||
)
|
||||
|
||||
def search_query(title, artist, playlist) -> bool:
|
||||
|
|
|
@ -45,7 +45,7 @@ class Database:
|
|||
)
|
||||
command = f"CREATE TABLE {self.name} ({params})"
|
||||
|
||||
logger.debug(f"executing {command}")
|
||||
logger.debug("executing %s", command)
|
||||
|
||||
conn.execute(command)
|
||||
|
||||
|
|
2382
streamrip/clients.py
2382
streamrip/clients.py
File diff suppressed because it is too large
Load Diff
|
@ -253,7 +253,7 @@ class Track(Media):
|
|||
|
||||
self.path = os.path.join(gettempdir(), f"{hash(self.id)}_{self.quality}.tmp")
|
||||
|
||||
def download(
|
||||
def download( # noqa
|
||||
self,
|
||||
quality: int = 3,
|
||||
parent_folder: str = "StreamripDownloads",
|
||||
|
@ -466,7 +466,7 @@ class Track(Media):
|
|||
def download_cover(self, width=999999, height=999999):
|
||||
"""Download the cover art, if cover_url is given."""
|
||||
self.cover_path = os.path.join(gettempdir(), f"cover{hash(self.cover_url)}.jpg")
|
||||
logger.debug(f"Downloading cover from {self.cover_url}")
|
||||
logger.debug("Downloading cover from %s", self.cover_url)
|
||||
|
||||
if not os.path.exists(self.cover_path):
|
||||
_cover_download(self.cover_url, self.cover_path)
|
||||
|
@ -549,7 +549,7 @@ class Track(Media):
|
|||
cover_url=cover_url,
|
||||
)
|
||||
|
||||
def tag(
|
||||
def tag( # noqa
|
||||
self,
|
||||
album_meta: dict = None,
|
||||
cover: Union[Picture, APIC, MP4Cover] = None,
|
||||
|
@ -783,6 +783,8 @@ class Track(Media):
|
|||
class Video(Media):
|
||||
"""Only for Tidal."""
|
||||
|
||||
downloaded_ids: set = set()
|
||||
|
||||
def __init__(self, client: Client, id: str, **kwargs):
|
||||
"""Initialize a Video object.
|
||||
|
||||
|
@ -1191,7 +1193,7 @@ class Tracklist(list):
|
|||
kwargs["sampling_rate"],
|
||||
)
|
||||
else:
|
||||
logger.debug(f"Downsampling to {sr/1000}kHz")
|
||||
logger.debug("Downsampling to %skHz", sr / 1000)
|
||||
|
||||
for track in self:
|
||||
track.convert(codec, **kwargs)
|
||||
|
@ -1426,26 +1428,27 @@ class Album(Tracklist, Media):
|
|||
self.download_message()
|
||||
|
||||
# choose optimal cover size and download it
|
||||
secho("Downloading cover art", bold=True)
|
||||
cover_path = os.path.join(gettempdir(), f"cover_{hash(self)}.jpg")
|
||||
embed_cover_size = kwargs.get("embed_cover_size", "large")
|
||||
secho(f"Downloading cover art ({embed_cover_size})", bold=True)
|
||||
|
||||
assert (
|
||||
embed_cover_size in self.cover_urls
|
||||
), f"Invalid cover size. Must be in {self.cover_urls.keys()}"
|
||||
|
||||
embed_cover_url = self.cover_urls[embed_cover_size]
|
||||
logger.debug("Chosen cover url: %s", embed_cover_url)
|
||||
if not os.path.exists(cover_path):
|
||||
cover_url = (
|
||||
embed_cover_url
|
||||
if embed_cover_url is None
|
||||
else tuple(filter(None, self.cover_urls.values()))[0]
|
||||
)
|
||||
if embed_cover_url is None:
|
||||
embed_cover_url = next(filter(None, self.cover_urls.values()))
|
||||
|
||||
_cover_download(cover_url, cover_path)
|
||||
logger.debug("Downloading cover from url %s", embed_cover_url)
|
||||
|
||||
_cover_download(embed_cover_url, cover_path)
|
||||
|
||||
hires_cov_path = os.path.join(self.folder, "cover.jpg")
|
||||
if kwargs.get("keep_hires_cover", True) and not os.path.exists(hires_cov_path):
|
||||
logger.debug("Downloading hires cover")
|
||||
_cover_download(self.cover_urls["original"], hires_cov_path)
|
||||
|
||||
cover_size = os.path.getsize(cover_path)
|
||||
|
@ -1464,6 +1467,7 @@ class Album(Tracklist, Media):
|
|||
)
|
||||
|
||||
if kwargs.get("embed_cover", True): # embed by default
|
||||
logger.debug("Getting cover_obj from %s", cover_path)
|
||||
# container generated when formatting folder name
|
||||
self.cover_obj = self.get_cover_obj(
|
||||
cover_path, self.container, self.client.source
|
||||
|
@ -1561,6 +1565,8 @@ class Album(Tracklist, Media):
|
|||
)
|
||||
if stat1 is not None and stat2 is not None
|
||||
)
|
||||
logger.debug("Sampling rate, bit depth = %s", stats)
|
||||
|
||||
if not stats:
|
||||
stats = (None, None)
|
||||
|
||||
|
@ -1769,7 +1775,7 @@ class Playlist(Tracklist, Media):
|
|||
)
|
||||
)
|
||||
|
||||
logger.debug(f"Loaded {len(self)} tracks from playlist {self.name}")
|
||||
logger.debug("Loaded %d tracks from playlist %s", len(self), self.name)
|
||||
|
||||
def _prepare_download(self, parent_folder: str = "StreamripDownloads", **kwargs):
|
||||
if kwargs.get("folder_format"):
|
||||
|
@ -1899,6 +1905,7 @@ class Artist(Tracklist, Media):
|
|||
:param kwargs:
|
||||
"""
|
||||
self.client = client
|
||||
self.downloaded_ids = set()
|
||||
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
@ -1971,8 +1978,8 @@ class Artist(Tracklist, Media):
|
|||
self.folder = parent_folder
|
||||
|
||||
logger.debug("Artist folder: %s", folder)
|
||||
logger.debug(f"Length of tracklist {len(self)}")
|
||||
logger.debug(f"Filters: {filters}")
|
||||
logger.debug("Length of tracklist %d", len(self))
|
||||
logger.debug("Filters: %s", filters)
|
||||
|
||||
final: Iterable
|
||||
if "repeats" in filters:
|
||||
|
@ -2008,10 +2015,14 @@ class Artist(Tracklist, Media):
|
|||
|
||||
kwargs.pop("parent_folder")
|
||||
# always an Album
|
||||
item.download(
|
||||
parent_folder=self.folder,
|
||||
**kwargs,
|
||||
)
|
||||
try:
|
||||
item.download(
|
||||
parent_folder=self.folder,
|
||||
**kwargs,
|
||||
)
|
||||
except PartialFailure:
|
||||
pass
|
||||
|
||||
self.downloaded_ids.update(item.downloaded_ids)
|
||||
|
||||
@property
|
||||
|
|
|
@ -63,6 +63,7 @@ class TrackMetadata:
|
|||
:type album: Optional[dict]
|
||||
"""
|
||||
# embedded information
|
||||
# TODO: add this to static attrs
|
||||
self.title: str
|
||||
self.album: str
|
||||
self.albumartist: str
|
||||
|
@ -193,13 +194,14 @@ class TrackMetadata:
|
|||
self.albumartist = safe_get(resp, "artist", "name")
|
||||
self.label = resp.get("label")
|
||||
self.url = resp.get("link")
|
||||
self.explicit = bool(resp.get("parental_warning"))
|
||||
|
||||
# not embedded
|
||||
self.explicit = bool(resp.get("parental_warning"))
|
||||
self.quality = 2
|
||||
self.bit_depth = None
|
||||
self.bit_depth = 16
|
||||
self.sampling_rate = 44100
|
||||
|
||||
self.cover_urls = get_cover_urls(resp, self.__source)
|
||||
self.sampling_rate = None
|
||||
self.streamable = True
|
||||
|
||||
elif self.__source == "soundcloud":
|
||||
|
@ -345,7 +347,7 @@ class TrackMetadata:
|
|||
genres: Iterable = re.findall(r"([^\u2192\/]+)", "/".join(self._genres))
|
||||
genres = set(genres)
|
||||
elif self.__source == "deezer":
|
||||
genres = ", ".join(g["name"] for g in self._genres)
|
||||
genres = (g["name"] for g in self._genres)
|
||||
|
||||
return ", ".join(genres)
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ Credits to Dash for this tool.
|
|||
import base64
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
from typing import List
|
||||
|
||||
import requests
|
||||
|
||||
|
@ -50,7 +51,7 @@ class Spoofer:
|
|||
|
||||
raise Exception("Could not find app id.")
|
||||
|
||||
def get_secrets(self):
|
||||
def get_secrets(self) -> List[str]:
|
||||
"""Get secrets."""
|
||||
seed_matches = re.finditer(self.seed_timezone_regex, self.bundle)
|
||||
secrets = OrderedDict()
|
||||
|
@ -81,6 +82,6 @@ class Spoofer:
|
|||
"".join(secrets[secret_pair])[:-44]
|
||||
).decode("utf-8")
|
||||
|
||||
vals = list(secrets.values())
|
||||
vals: List[str] = list(secrets.values())
|
||||
vals.remove("")
|
||||
return vals
|
||||
|
|
|
@ -180,6 +180,11 @@ __QUALITY_MAP: Dict[str, Dict[int, Union[int, str, Tuple[int, str]]]] = {
|
|||
2: "LOSSLESS", # CD Quality
|
||||
3: "HI_RES", # MQA
|
||||
},
|
||||
"deezloader": {
|
||||
0: 128,
|
||||
1: 320,
|
||||
2: 1411,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue