Formatting

Signed-off-by: nathom <nathanthomas707@gmail.com>
This commit is contained in:
nathom 2021-06-19 18:57:50 -07:00
parent 76ba2d413b
commit 7698ad7a2e
5 changed files with 39 additions and 112 deletions

View file

@ -7,7 +7,7 @@ from getpass import getpass
from hashlib import md5 from hashlib import md5
import click import click
import requests # type: ignore import requests
from . import __version__ from . import __version__
from .clients import TidalClient from .clients import TidalClient
@ -15,7 +15,6 @@ from .config import Config
from .constants import CACHE_DIR, CONFIG_DIR, CONFIG_PATH, QOBUZ_FEATURED_KEYS from .constants import CACHE_DIR, CONFIG_DIR, CONFIG_PATH, QOBUZ_FEATURED_KEYS
from .core import MusicDL from .core import MusicDL
logging.basicConfig(level="WARNING") logging.basicConfig(level="WARNING")
logger = logging.getLogger("streamrip") logger = logging.getLogger("streamrip")
@ -26,9 +25,7 @@ if not os.path.isdir(CACHE_DIR):
@click.group(invoke_without_command=True) @click.group(invoke_without_command=True)
@click.option( @click.option("-c", "--convert", metavar="CODEC", help="alac, mp3, flac, or ogg")
"-c", "--convert", metavar="CODEC", help="alac, mp3, flac, or ogg"
)
@click.option( @click.option(
"-u", "-u",
"--urls", "--urls",
@ -141,9 +138,7 @@ def filter_discography(ctx, **kwargs):
@cli.command() @cli.command()
@click.option( @click.option("-t", "--type", default="album", help="album, playlist, track, or artist")
"-t", "--type", default="album", help="album, playlist, track, or artist"
)
@click.option( @click.option(
"-s", "-s",
"--source", "--source",
@ -264,9 +259,7 @@ def lastfm(ctx, source, url):
@cli.command() @cli.command()
@click.option("-o", "--open", is_flag=True, help="Open the config file") @click.option("-o", "--open", is_flag=True, help="Open the config file")
@click.option( @click.option("-d", "--directory", is_flag=True, help="Open the config directory")
"-d", "--directory", is_flag=True, help="Open the config directory"
)
@click.option("-q", "--qobuz", is_flag=True, help="Set Qobuz credentials") @click.option("-q", "--qobuz", is_flag=True, help="Set Qobuz credentials")
@click.option("-t", "--tidal", is_flag=True, help="Re-login into Tidal") @click.option("-t", "--tidal", is_flag=True, help="Re-login into Tidal")
@click.option("--reset", is_flag=True, help="RESET the config file") @click.option("--reset", is_flag=True, help="RESET the config file")
@ -310,9 +303,7 @@ def config(ctx, **kwargs):
click.launch(config_dir) click.launch(config_dir)
if kwargs["qobuz"]: if kwargs["qobuz"]:
config.file["qobuz"]["email"] = input( config.file["qobuz"]["email"] = input(click.style("Qobuz email: ", fg="blue"))
click.style("Qobuz email: ", fg="blue")
)
click.secho("Qobuz password (will not show on screen):", fg="blue") click.secho("Qobuz password (will not show on screen):", fg="blue")
config.file["qobuz"]["password"] = md5( config.file["qobuz"]["password"] = md5(
@ -320,9 +311,7 @@ def config(ctx, **kwargs):
).hexdigest() ).hexdigest()
config.save() config.save()
click.secho( click.secho("Qobuz credentials hashed and saved to config.", fg="green")
"Qobuz credentials hashed and saved to config.", fg="green"
)
if kwargs["tidal"]: if kwargs["tidal"]:
client = TidalClient() client = TidalClient()

View file

@ -216,9 +216,7 @@ class QobuzClient(Client):
:rtype: dict :rtype: dict
""" """
page, status_code = self._api_request(epoint, params) page, status_code = self._api_request(epoint, params)
logger.debug( logger.debug("Keys returned from _gen_pages: %s", ", ".join(page.keys()))
"Keys returned from _gen_pages: %s", ", ".join(page.keys())
)
key = epoint.split("/")[0] + "s" key = epoint.split("/")[0] + "s"
total = page.get(key, {}) total = page.get(key, {})
total = total.get("total") or total.get("items") total = total.get("total") or total.get("items")
@ -242,9 +240,7 @@ class QobuzClient(Client):
for secret in self.secrets: for secret in self.secrets:
if self._test_secret(secret): if self._test_secret(secret):
self.sec = secret self.sec = secret
logger.debug( logger.debug("Working secret and app_id: %s - %s", secret, self.app_id)
"Working secret and app_id: %s - %s", secret, self.app_id
)
break break
if not hasattr(self, "sec"): if not hasattr(self, "sec"):
raise InvalidAppSecretError(f"Invalid secrets: {self.secrets}") raise InvalidAppSecretError(f"Invalid secrets: {self.secrets}")
@ -278,15 +274,11 @@ class QobuzClient(Client):
response, status_code = self._api_request(epoint, params) response, status_code = self._api_request(epoint, params)
if status_code != 200: if status_code != 200:
raise Exception( raise Exception(f'Error fetching metadata. "{response["message"]}"')
f'Error fetching metadata. "{response["message"]}"'
)
return response return response
def _api_search( def _api_search(self, query: str, media_type: str, limit: int = 500) -> Generator:
self, query: str, media_type: str, limit: int = 500
) -> Generator:
"""Send a search request to the API. """Send a search request to the API.
:param query: :param query:
@ -338,18 +330,14 @@ class QobuzClient(Client):
resp, status_code = self._api_request(epoint, params) resp, status_code = self._api_request(epoint, params)
if status_code == 401: if status_code == 401:
raise AuthenticationError( raise AuthenticationError(f"Invalid credentials from params {params}")
f"Invalid credentials from params {params}"
)
elif status_code == 400: elif status_code == 400:
raise InvalidAppIdError(f"Invalid app id from params {params}") raise InvalidAppIdError(f"Invalid app id from params {params}")
else: else:
logger.info("Logged in to Qobuz") logger.info("Logged in to Qobuz")
if not resp["user"]["credential"]["parameters"]: if not resp["user"]["credential"]["parameters"]:
raise IneligibleError( raise IneligibleError("Free accounts are not eligible to download tracks.")
"Free accounts are not eligible to download tracks."
)
self.uat = resp["user_auth_token"] self.uat = resp["user_auth_token"]
self.session.headers.update({"X-User-Auth-Token": self.uat}) self.session.headers.update({"X-User-Auth-Token": self.uat})
@ -398,9 +386,7 @@ class QobuzClient(Client):
} }
response, status_code = self._api_request("track/getFileUrl", params) response, status_code = self._api_request("track/getFileUrl", params)
if status_code == 400: if status_code == 400:
raise InvalidAppSecretError( raise InvalidAppSecretError("Invalid app secret from params %s" % params)
"Invalid app secret from params %s" % params
)
return response return response
@ -418,9 +404,7 @@ class QobuzClient(Client):
try: try:
return r.json(), r.status_code return r.json(), r.status_code
except Exception: except Exception:
logger.error( logger.error("Problem getting JSON. Status code: %s", r.status_code)
"Problem getting JSON. Status code: %s", r.status_code
)
raise raise
def _test_secret(self, secret: str) -> bool: def _test_secret(self, secret: str) -> bool:
@ -451,9 +435,7 @@ class DeezerClient(Client):
# no login required # no login required
self.logged_in = True self.logged_in = True
def search( def search(self, query: str, media_type: str = "album", limit: int = 200) -> dict:
self, query: str, media_type: str = "album", limit: int = 200
) -> dict:
"""Search API for query. """Search API for query.
:param query: :param query:
@ -490,9 +472,7 @@ class DeezerClient(Client):
url = f"{DEEZER_BASE}/{media_type}/{meta_id}" url = f"{DEEZER_BASE}/{media_type}/{meta_id}"
item = self.session.get(url).json() item = self.session.get(url).json()
if media_type in ("album", "playlist"): if media_type in ("album", "playlist"):
tracks = self.session.get( tracks = self.session.get(f"{url}/tracks", params={"limit": 1000}).json()
f"{url}/tracks", params={"limit": 1000}
).json()
item["tracks"] = tracks["data"] item["tracks"] = tracks["data"]
item["track_total"] = len(tracks["data"]) item["track_total"] = len(tracks["data"])
elif media_type == "artist": elif media_type == "artist":
@ -588,9 +568,7 @@ class TidalClient(Client):
logger.debug(resp) logger.debug(resp)
return resp return resp
def search( def search(self, query: str, media_type: str = "album", limit: int = 100) -> dict:
self, query: str, media_type: str = "album", limit: int = 100
) -> dict:
"""Search for a query. """Search for a query.
:param query: :param query:
@ -619,19 +597,13 @@ class TidalClient(Client):
return self._get_video_stream_url(track_id) return self._get_video_stream_url(track_id)
params = { params = {
"audioquality": get_quality( "audioquality": get_quality(min(quality, TIDAL_MAX_Q), self.source),
min(quality, TIDAL_MAX_Q), self.source
),
"playbackmode": "STREAM", "playbackmode": "STREAM",
"assetpresentation": "FULL", "assetpresentation": "FULL",
} }
resp = self._api_request( resp = self._api_request(f"tracks/{track_id}/playbackinfopostpaywall", params)
f"tracks/{track_id}/playbackinfopostpaywall", params
)
try: try:
manifest = json.loads( manifest = json.loads(base64.b64decode(resp["manifest"]).decode("utf-8"))
base64.b64decode(resp["manifest"]).decode("utf-8")
)
except KeyError: except KeyError:
raise Exception(resp["userMessage"]) raise Exception(resp["userMessage"])
@ -837,9 +809,7 @@ class TidalClient(Client):
offset += 100 offset += 100
tracks_left -= 100 tracks_left -= 100
resp["items"].extend( resp["items"].extend(
self._api_request(f"{url}/items", {"offset": offset})[ self._api_request(f"{url}/items", {"offset": offset})["items"]
"items"
]
) )
item["tracks"] = [item["item"] for item in resp["items"]] item["tracks"] = [item["item"] for item in resp["items"]]
@ -884,9 +854,7 @@ class TidalClient(Client):
r'#EXT-X-STREAM-INF:BANDWIDTH=\d+,AVERAGE-BANDWIDTH=\d+,CODECS="[^"]+"' r'#EXT-X-STREAM-INF:BANDWIDTH=\d+,AVERAGE-BANDWIDTH=\d+,CODECS="[^"]+"'
r",RESOLUTION=\d+x\d+\n(.+)" r",RESOLUTION=\d+x\d+\n(.+)"
) )
manifest = json.loads( manifest = json.loads(base64.b64decode(resp["manifest"]).decode("utf-8"))
base64.b64decode(resp["manifest"]).decode("utf-8")
)
available_urls = self.session.get(manifest["urls"][0]) available_urls = self.session.get(manifest["urls"][0])
url_info = re.findall(stream_url_regex, available_urls.text) url_info = re.findall(stream_url_regex, available_urls.text)
@ -965,10 +933,7 @@ class SoundCloudClient(Client):
url = None url = None
for tc in track["media"]["transcodings"]: for tc in track["media"]["transcodings"]:
fmt = tc["format"] fmt = tc["format"]
if ( if fmt["protocol"] == "hls" and fmt["mime_type"] == "audio/mpeg":
fmt["protocol"] == "hls"
and fmt["mime_type"] == "audio/mpeg"
):
url = tc["url"] url = tc["url"]
break break

View file

@ -335,9 +335,7 @@ class MusicDL(list):
parsed.extend(self.url_parse.findall(url)) # Qobuz, Tidal, Dezer parsed.extend(self.url_parse.findall(url)) # Qobuz, Tidal, Dezer
soundcloud_urls = self.soundcloud_url_parse.findall(url) soundcloud_urls = self.soundcloud_url_parse.findall(url)
soundcloud_items = [ soundcloud_items = [self.clients["soundcloud"].get(u) for u in soundcloud_urls]
self.clients["soundcloud"].get(u) for u in soundcloud_urls
]
parsed.extend( parsed.extend(
("soundcloud", item["kind"], url) ("soundcloud", item["kind"], url)
@ -369,15 +367,11 @@ class MusicDL(list):
# For testing: # For testing:
# https://www.last.fm/user/nathan3895/playlists/12058911 # https://www.last.fm/user/nathan3895/playlists/12058911
user_regex = re.compile( user_regex = re.compile(r"https://www\.last\.fm/user/([^/]+)/playlists/\d+")
r"https://www\.last\.fm/user/([^/]+)/playlists/\d+"
)
lastfm_urls = self.lastfm_url_parse.findall(urls) lastfm_urls = self.lastfm_url_parse.findall(urls)
try: try:
lastfm_source = self.config.session["lastfm"]["source"] lastfm_source = self.config.session["lastfm"]["source"]
lastfm_fallback_source = self.config.session["lastfm"][ lastfm_fallback_source = self.config.session["lastfm"]["fallback_source"]
"fallback_source"
]
except KeyError: except KeyError:
self._config_updating_message() self._config_updating_message()
self.config.update() self.config.update()
@ -407,9 +401,7 @@ class MusicDL(list):
except (NoResultsFound, StopIteration): except (NoResultsFound, StopIteration):
return None return None
track = try_search(lastfm_source) or try_search( track = try_search(lastfm_source) or try_search(lastfm_fallback_source)
lastfm_fallback_source
)
if track is None: if track is None:
return False return False
@ -431,9 +423,7 @@ class MusicDL(list):
pl.creator = creator_match.group(1) pl.creator = creator_match.group(1)
tracks_not_found = 0 tracks_not_found = 0
with concurrent.futures.ThreadPoolExecutor( with concurrent.futures.ThreadPoolExecutor(max_workers=15) as executor:
max_workers=15
) as executor:
futures = [ futures = [
executor.submit(search_query, title, artist, pl) executor.submit(search_query, title, artist, pl)
for title, artist in queries for title, artist in queries
@ -450,9 +440,7 @@ class MusicDL(list):
pl.loaded = True pl.loaded = True
if tracks_not_found > 0: if tracks_not_found > 0:
click.secho( click.secho(f"{tracks_not_found} tracks not found.", fg="yellow")
f"{tracks_not_found} tracks not found.", fg="yellow"
)
self.append(pl) self.append(pl)
def handle_txt(self, filepath: Union[str, os.PathLike]): def handle_txt(self, filepath: Union[str, os.PathLike]):
@ -507,9 +495,7 @@ class MusicDL(list):
else: else:
logger.debug("Not generator") logger.debug("Not generator")
items = ( items = (
results.get("data") results.get("data") or results.get("items") or results.get("collection")
or results.get("items")
or results.get("collection")
) )
if items is None: if items is None:
raise NoResultsFound(query) raise NoResultsFound(query)
@ -549,9 +535,7 @@ class MusicDL(list):
raise NotImplementedError raise NotImplementedError
fields = (fname for _, fname, _, _ in Formatter().parse(fmt) if fname) fields = (fname for _, fname, _, _ in Formatter().parse(fmt) if fname)
ret = fmt.format( ret = fmt.format(**{k: media.get(k, default="Unknown") for k in fields})
**{k: media.get(k, default="Unknown") for k in fields}
)
return ret return ret
def interactive_search( # noqa def interactive_search( # noqa
@ -682,9 +666,7 @@ class MusicDL(list):
playlist_title = html.unescape(playlist_title_match.group(1)) playlist_title = html.unescape(playlist_title_match.group(1))
if remaining_tracks > 0: if remaining_tracks > 0:
with concurrent.futures.ThreadPoolExecutor( with concurrent.futures.ThreadPoolExecutor(max_workers=15) as executor:
max_workers=15
) as executor:
last_page = int(remaining_tracks // 50) + int( last_page = int(remaining_tracks // 50) + int(
remaining_tracks % 50 != 0 remaining_tracks % 50 != 0
) )
@ -726,7 +708,8 @@ class MusicDL(list):
self.config.save() self.config.save()
click.secho( click.secho(
f'Credentials saved to config file at "{self.config._path}"' f'Credentials saved to config file at "{self.config._path}"',
fg="green",
) )
else: else:
raise Exception raise Exception

View file

@ -25,9 +25,7 @@ class MusicDB:
"""Create a database at `self.path`.""" """Create a database at `self.path`."""
with sqlite3.connect(self.path) as conn: with sqlite3.connect(self.path) as conn:
try: try:
conn.execute( conn.execute("CREATE TABLE downloads (id TEXT UNIQUE NOT NULL);")
"CREATE TABLE downloads (id TEXT UNIQUE NOT NULL);"
)
logger.debug("Download-IDs database created: %s", self.path) logger.debug("Download-IDs database created: %s", self.path)
except sqlite3.OperationalError: except sqlite3.OperationalError:
pass pass

View file

@ -82,9 +82,7 @@ def get_quality(quality_id: int, source: str) -> Union[str, int]:
raise InvalidSourceError(source) raise InvalidSourceError(source)
possible_keys = set(q_map.keys()) possible_keys = set(q_map.keys())
assert ( assert quality_id in possible_keys, f"{quality_id} must be in {possible_keys}"
quality_id in possible_keys
), f"{quality_id} must be in {possible_keys}"
return q_map[quality_id] return q_map[quality_id]
@ -125,9 +123,7 @@ def get_stats_from_quality(
raise InvalidQuality(quality_id) raise InvalidQuality(quality_id)
def tqdm_download( def tqdm_download(url: str, filepath: str, params: dict = None, desc: str = None):
url: str, filepath: str, params: dict = None, desc: str = None
):
"""Download a file with a progress bar. """Download a file with a progress bar.
:param url: url to direct download :param url: url to direct download
@ -199,9 +195,7 @@ def tidal_cover_url(uuid, size):
possibles = (80, 160, 320, 640, 1280) possibles = (80, 160, 320, 640, 1280)
assert size in possibles, f"size must be in {possibles}" assert size in possibles, f"size must be in {possibles}"
return TIDAL_COVER_URL.format( return TIDAL_COVER_URL.format(uuid=uuid.replace("-", "/"), height=size, width=size)
uuid=uuid.replace("-", "/"), height=size, width=size
)
def init_log(path: Optional[str] = None, level: str = "DEBUG"): def init_log(path: Optional[str] = None, level: str = "DEBUG"):
@ -303,9 +297,7 @@ def gen_threadsafe_session(
headers = {} headers = {}
session = requests.Session() session = requests.Session()
adapter = requests.adapters.HTTPAdapter( adapter = requests.adapters.HTTPAdapter(pool_connections=100, pool_maxsize=100)
pool_connections=100, pool_maxsize=100
)
session.mount("https://", adapter) session.mount("https://", adapter)
session.headers.update(headers) session.headers.update(headers)
return session return session