Merge branch 'dev'

This commit is contained in:
Nathan Thomas 2021-08-13 16:19:28 -07:00
commit 362919068c
10 changed files with 1296 additions and 1222 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

BIN
demo/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -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():

View File

@ -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:

View File

@ -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)

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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,
},
}