Begin move to cleo

This commit is contained in:
nathom 2021-07-30 11:20:36 -07:00
parent 54f4ab99af
commit 9970ac548f
13 changed files with 528 additions and 150 deletions

View file

@ -9,15 +9,31 @@ logging.basicConfig(level="WARNING")
logger = logging.getLogger("streamrip")
@click.group(invoke_without_command=True)
class SkipArg(click.Group):
def parse_args(self, ctx, args):
if len(args) == 0:
click.echo(self.get_help(ctx))
exit()
if args[0] in self.commands:
print('command found')
args.insert(0, "")
# if args[0] in self.commands:
# if len(args) == 1 or args[1] not in self.commands:
# # This condition needs updating for multiple positional arguments
# args.insert(0, "")
super(SkipArg, self).parse_args(ctx, args)
# @click.option(
# "-u",
# "--urls",
# metavar="URLS",
# help="Url from Qobuz, Tidal, SoundCloud, or Deezer",
# multiple=True,
# )
@click.group(cls=SkipArg, invoke_without_command=True)
@click.option("-c", "--convert", metavar="CODEC", help="alac, mp3, flac, or ogg")
@click.option(
"-u",
"--urls",
metavar="URLS",
help="Url from Qobuz, Tidal, SoundCloud, or Deezer",
multiple=True,
)
@click.option(
"-q",
"--quality",
@ -27,6 +43,7 @@ logger = logging.getLogger("streamrip")
@click.option("-t", "--text", metavar="PATH", help="Download urls from a text file.")
@click.option("-nd", "--no-db", is_flag=True, help="Ignore the database.")
@click.option("--debug", is_flag=True, help="Show debugging logs.")
@click.argument("URLS", nargs=1)
@click.version_option(prog_name="rip", version=__version__)
@click.pass_context
def cli(ctx, **kwargs):
@ -54,9 +71,7 @@ def cli(ctx, **kwargs):
from .constants import CONFIG_DIR
from .core import RipCore
logging.basicConfig(level="WARNING")
logger = logging.getLogger("streamrip")
print(kwargs)
if not os.path.isdir(CONFIG_DIR):
os.makedirs(CONFIG_DIR, exist_ok=True)
@ -68,7 +83,8 @@ def cli(ctx, **kwargs):
logger.debug("Starting debug log")
if ctx.invoked_subcommand is None and not ctx.params["urls"]:
echo(cli.get_help(ctx))
print(dir(cli))
click.echo(cli.get_help(ctx))
if ctx.invoked_subcommand not in {
None,
@ -90,13 +106,13 @@ def cli(ctx, **kwargs):
r = requests.get("https://pypi.org/pypi/streamrip/json").json()
newest = r["info"]["version"]
if __version__ != newest:
secho(
click.secho(
"A new version of streamrip is available! "
"Run `pip3 install streamrip --upgrade` to update.",
fg="yellow",
)
else:
secho("streamrip is up-to-date!", fg="green")
click.secho("streamrip is up-to-date!", fg="green")
if kwargs["no_db"]:
config.session["database"]["enabled"] = False
@ -108,7 +124,7 @@ def cli(ctx, **kwargs):
if kwargs["quality"] is not None:
quality = int(kwargs["quality"])
if quality not in range(5):
secho("Invalid quality", fg="red")
click.secho("Invalid quality", fg="red")
return
config.session["qobuz"]["quality"] = quality
@ -126,7 +142,7 @@ def cli(ctx, **kwargs):
logger.debug(f"Handling {kwargs['text']}")
core.handle_txt(kwargs["text"])
else:
secho(f"Text file {kwargs['text']} does not exist.")
click.secho(f"Text file {kwargs['text']} does not exist.")
if ctx.invoked_subcommand is None:
core.download()
@ -150,6 +166,7 @@ def filter_discography(ctx, **kwargs):
For basic filtering, use the `--repeats` and `--features` filters.
"""
raise Exception
filters = kwargs.copy()
filters.pop("urls")
config.session["filters"] = filters
@ -206,7 +223,7 @@ def search(ctx, **kwargs):
if core.interactive_search(query, kwargs["source"], kwargs["type"]):
core.download()
else:
secho("No items chosen, exiting.", fg="bright_red")
click.secho("No items chosen, exiting.", fg="bright_red")
@cli.command()
@ -333,10 +350,10 @@ def config(ctx, **kwargs):
config.update()
if kwargs["path"]:
echo(CONFIG_PATH)
click.echo(CONFIG_PATH)
if kwargs["open"]:
secho(f"Opening {CONFIG_PATH}", fg="green")
click.secho(f"Opening {CONFIG_PATH}", fg="green")
click.launch(CONFIG_PATH)
if kwargs["open_vim"]:
@ -347,41 +364,41 @@ def config(ctx, **kwargs):
if kwargs["directory"]:
config_dir = os.path.dirname(CONFIG_PATH)
secho(f"Opening {config_dir}", fg="green")
click.secho(f"Opening {config_dir}", fg="green")
click.launch(config_dir)
if kwargs["qobuz"]:
config.file["qobuz"]["email"] = input(style("Qobuz email: ", fg="blue"))
config.file["qobuz"]["email"] = input(click.style("Qobuz email: ", fg="blue"))
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(
getpass(prompt="").encode("utf-8")
).hexdigest()
config.save()
secho("Qobuz credentials hashed and saved to config.", fg="green")
click.secho("Qobuz credentials hashed and saved to config.", fg="green")
if kwargs["tidal"]:
client = TidalClient()
client.login()
config.file["tidal"].update(client.get_tokens())
config.save()
secho("Credentials saved to config.", fg="green")
click.secho("Credentials saved to config.", fg="green")
if kwargs["deezer"]:
secho(
click.secho(
"If you're not sure how to find the ARL cookie, see the instructions at ",
italic=True,
nl=False,
dim=True,
)
secho(
click.secho(
"https://github.com/nathom/streamrip/wiki/Finding-your-Deezer-ARL-Cookie",
underline=True,
italic=True,
fg="blue",
)
config.file["deezer"]["arl"] = input(style("ARL: ", fg="green"))
config.file["deezer"]["arl"] = input(click.style("ARL: ", fg="green"))
config.save()
@ -481,7 +498,7 @@ def convert(ctx, **kwargs):
elif os.path.isfile(kwargs["path"]):
codec_map[codec](filename=kwargs["path"], **converter_args).convert()
else:
secho(f"File {kwargs['path']} does not exist.", fg="red")
click.secho(f"File {kwargs['path']} does not exist.", fg="red")
@cli.command()
@ -503,7 +520,7 @@ def repair(ctx, **kwargs):
def none_chosen():
"""Print message if nothing was chosen."""
secho("No items chosen, exiting.", fg="bright_red")
click.secho("No items chosen, exiting.", fg="bright_red")
def main():

214
rip/cli_cleo.py Normal file
View file

@ -0,0 +1,214 @@
import logging
import os
from cleo.application import Application as BaseApplication
from cleo.commands.command import Command
from streamrip import __version__
from .config import Config
from .core import RipCore
logging.basicConfig(level="WARNING")
logger = logging.getLogger("streamrip")
class DownloadCommand(Command):
"""
Download items from a url
url
{--f|file=None : Path to a text file containing urls}
{urls?* : One or more Qobuz, Tidal, Deezer, or SoundCloud urls}
"""
help = (
'\nDownload "Dreams" by Fleetwood Mac:\n'
"$ <fg=magenta>rip url https://www.deezer.com/en/track/63480987</>\n\n"
"Batch download urls from a text file named urls.txt:\n"
"$ <fg=magenta>rip --file urls.txt</>\n"
)
def handle(self):
config = Config()
core = RipCore(config)
urls = self.argument("urls")
path = self.option("file")
if path != "None":
if os.path.isfile(path):
core.handle_txt(path)
else:
self.line(
f"<error>File <comment>{path}</comment> does not exist.</error>"
)
return 1
if urls:
core.handle_urls(";".join(urls))
if len(core) > 0:
core.download()
elif not urls and path == "None":
self.line("<error>Must pass arguments. See </><info>rip url -h</info>.")
return 0
class SearchCommand(Command):
"""
Search for and download items in interactive mode.
search
{query : The name to search for}
{--s|source=qobuz : Qobuz, Tidal, Soundcloud, Deezer, or Deezloader}
{--t|type=album : Album, Playlist, Track, or Artist}
"""
def handle(self):
query = self.argument("query")
source, type = clean_options(self.option("source"), self.option("type"))
config = Config()
core = RipCore(config)
if core.interactive_search(query, source, type):
core.download()
else:
self.line("<error>No items chosen, exiting.</error>")
class DiscoverCommand(Command):
"""
Browse and download items in interactive mode.
discover
{--s|scrape : Download all of the items in the list}
{--m|max-items=50 : The number of items to fetch}
{list=ideal-discography : The list to fetch}
"""
help = (
"\nAvailable options for <info>list</info>:\n\n"
" • most-streamed\n"
" • recent-releases\n"
" • best-sellers\n"
" • press-awards\n"
" • ideal-discography\n"
" • editor-picks\n"
" • most-featured\n"
" • qobuzissims\n"
" • new-releases\n"
" • new-releases-full\n"
" • harmonia-mundi\n"
" • universal-classic\n"
" • universal-jazz\n"
" • universal-jeunesse\n"
" • universal-chanson\n"
)
def handle(self):
from streamrip.constants import QOBUZ_FEATURED_KEYS
chosen_list = self.argument("list")
scrape = self.option("scrape")
max_items = self.option("max-items")
if chosen_list not in QOBUZ_FEATURED_KEYS:
self.line(f'<error>Error: list "{chosen_list}" not available</error>')
self.line(self.help)
return 1
config = Config()
core = RipCore(config)
if scrape:
core.scrape(chosen_list, max_items)
core.download()
return 0
if core.interactive_search(
chosen_list, "qobuz", "featured", limit=int(max_items)
):
core.download()
else:
self.line("<error>No items chosen, exiting.</error>")
class LastfmCommand(Command):
"""
Search for tracks from a list.fm playlist and download them.
lastfm
{--s|source=qobuz : The source to search for items on}
{urls* : Last.fm playlist urls}
"""
def handle(self):
source = self.option("source")
urls = self.argument("urls")
config = Config()
core = RipCore(config)
config.session["lastfm"]["source"] = source
core.handle_lastfm_urls(";".join(urls))
core.download()
class ConfigCommand(Command):
"""
Manage the configuration file
{--o|open : Open the config file in the default application}
{--ov|open-vim : Open the config file in (neo)vim}
{--d|directory : Open the directory that the config file is located in}
{--p|path : Show the config file's path}
{--q|qobuz : Set the credentials for Qobuz}
{--t|tidal : Log into Tidal}
{--dz|deezer : Set the Deezer ARL}
{--reset : Reset the config file}
{--update : Reset the config file, keeping the credentials}
"""
class Application(BaseApplication):
def __init__(self):
super().__init__("rip", __version__)
def _run(self, io):
if io.is_debug():
logger.setLevel(logging.DEBUG)
super()._run(io)
# @property
# def _default_definition(self):
# default_globals = super()._default_definition
# default_globals.add_option(Option("convert", shortcut="c", flag=False))
# return default_globals
# class ConvertCommand(Command):
# pass
# class RepairCommand(Command):
# pass
def clean_options(*opts):
return tuple(o.replace("=", "").strip() for o in opts)
def main():
application = Application()
application.add(DownloadCommand())
application.add(SearchCommand())
application.add(DiscoverCommand())
application.add(LastfmCommand())
# application.add(ConfigCommand())
application.run()
if __name__ == "__main__":
main()

View file

@ -7,7 +7,7 @@ import shutil
from pprint import pformat
from typing import Any, Dict
import click
from click import style, secho
import tomlkit
from streamrip.exceptions import InvalidSourceError

View file

@ -4,7 +4,7 @@ import os
import re
from pathlib import Path
import click
from click import style, secho
APPNAME = "streamrip"
APP_DIR = click.get_app_dir(APPNAME)

View file

@ -10,7 +10,7 @@ from hashlib import md5
from string import Formatter
from typing import Dict, Generator, List, Optional, Tuple, Type, Union
import click
from click import style, secho
import requests
from tqdm import tqdm
@ -162,8 +162,8 @@ class RipCore(list):
if not parsed and len(self) == 0:
if "last.fm" in url:
message = (
f"For last.fm urls, use the {style('lastfm', fg='yellow')} "
f"command. See {style('rip lastfm --help', fg='yellow')}."
f"For last.fm urls, use the {click.style('lastfm', fg='yellow')} "
f"command. See {click.style('rip lastfm --help', fg='yellow')}."
)
else:
message = f"Cannot find urls in text: {url}"
@ -175,7 +175,7 @@ class RipCore(list):
logger.info(
f"ID {item_id} already downloaded, use --no-db to override."
)
secho(
click.secho(
f"ID {item_id} already downloaded, use --no-db to override.",
fg="magenta",
)
@ -248,12 +248,12 @@ class RipCore(list):
max_items = float("inf")
if self.failed_db.is_dummy:
secho(
click.secho(
"Failed downloads database must be enabled in the config file "
"to repair!",
fg="red",
)
raise click.Abort
exit()
for counter, (source, media_type, item_id) in enumerate(self.failed_db):
if counter >= max_items:
@ -304,7 +304,7 @@ class RipCore(list):
item.load_meta(**arguments)
except NonStreamable:
self.failed_db.add((item.client.source, item.type, item.id))
secho(f"{item!s} is not available, skipping.", fg="red")
click.secho(f"{item!s} is not available, skipping.", fg="red")
continue
try:
@ -321,7 +321,7 @@ class RipCore(list):
self.failed_db.add(failed_item_info)
continue
except ItemExists as e:
secho(f'"{e!s}" already exists. Skipping.', fg="yellow")
click.secho(f'"{e!s}" already exists. Skipping.', fg="yellow")
continue
if hasattr(item, "id"):
@ -366,13 +366,13 @@ class RipCore(list):
creds = self.config.creds(client.source)
if client.source == "deezer" and creds["arl"] == "":
if self.config.session["deezer"]["deezloader_warnings"]:
secho(
click.secho(
"Falling back to Deezloader (max 320kbps MP3). If you have a subscription, run ",
nl=False,
fg="yellow",
)
secho("rip config --deezer ", nl=False, bold=True)
secho("to download FLAC files.\n\n", fg="yellow")
click.secho("rip config --deezer ", nl=False, bold=True)
click.secho("to download FLAC files.\n\n", fg="yellow")
raise DeezloaderFallback
while True:
@ -380,7 +380,7 @@ class RipCore(list):
client.login(**creds)
break
except AuthenticationError:
secho("Invalid credentials, try again.", fg="yellow")
click.secho("Invalid credentials, try again.", fg="yellow")
self.prompt_creds(client.source)
creds = self.config.creds(client.source)
except MissingCredentials:
@ -419,7 +419,7 @@ class RipCore(list):
interpreter_urls = QOBUZ_INTERPRETER_URL_REGEX.findall(url)
if interpreter_urls:
secho(
click.secho(
"Extracting IDs from Qobuz interpreter urls. Use urls "
"that include the artist ID for faster preprocessing.",
fg="yellow",
@ -432,7 +432,7 @@ class RipCore(list):
dynamic_urls = DEEZER_DYNAMIC_LINK_REGEX.findall(url)
if dynamic_urls:
secho(
click.secho(
"Extracting IDs from Deezer dynamic link. Use urls "
"of the form https://www.deezer.com/{country}/{type}/{id} for "
"faster processing.",
@ -486,7 +486,7 @@ class RipCore(list):
exit()
except Exception as err:
self._config_corrupted_message(err)
raise click.Abort
exit()
def search_query(title, artist, playlist) -> bool:
"""Search for a query and add the first result to playlist.
@ -523,8 +523,10 @@ class RipCore(list):
playlist.append(track)
return True
from streamrip.utils import TQDM_BAR_FORMAT
for purl in lastfm_urls:
secho(f"Fetching playlist at {purl}", fg="blue")
click.secho(f"Fetching playlist at {purl}", fg="blue")
title, queries = self.get_lastfm_playlist(purl)
pl = Playlist(client=self.get_client(lastfm_source), name=title)
@ -541,8 +543,11 @@ class RipCore(list):
# only for the progress bar
for search_attempt in tqdm(
concurrent.futures.as_completed(futures),
unit="Tracks",
dynamic_ncols=True,
total=len(futures),
desc="Searching",
desc="Searching...",
bar_format=TQDM_BAR_FORMAT,
):
if not search_attempt.result():
tracks_not_found += 1
@ -550,7 +555,8 @@ class RipCore(list):
pl.loaded = True
if tracks_not_found > 0:
secho(f"{tracks_not_found} tracks not found.", fg="yellow")
click.secho(f"{tracks_not_found} tracks not found.", fg="yellow")
self.append(pl)
def handle_txt(self, filepath: Union[str, os.PathLike]):
@ -602,7 +608,7 @@ class RipCore(list):
media_type if media_type != "featured" else "album"
].from_api(item, client)
if i > limit:
if i >= limit - 1:
return
else:
logger.debug("Not generator")
@ -616,7 +622,7 @@ class RipCore(list):
for i, item in enumerate(items):
logger.debug(item["title"])
yield MEDIA_CLASS[media_type].from_api(item, client) # type: ignore
if i > limit:
if i >= limit - 1:
return
def preview_media(self, media) -> str:
@ -795,11 +801,7 @@ class RipCore(list):
for page in range(1, last_page + 1)
]
for future in tqdm(
concurrent.futures.as_completed(futures),
total=len(futures),
desc="Scraping playlist",
):
for future in concurrent.futures.as_completed(futures):
get_titles(future.result().text)
return playlist_title, info
@ -815,9 +817,9 @@ class RipCore(list):
:type source: str
"""
if source == "qobuz":
secho("Enter Qobuz email:", fg="green")
click.secho("Enter Qobuz email:", fg="green")
self.config.file[source]["email"] = input()
secho(
click.secho(
"Enter Qobuz password (will not show on screen):",
fg="green",
)
@ -826,27 +828,27 @@ class RipCore(list):
).hexdigest()
self.config.save()
secho(
click.secho(
f'Credentials saved to config file at "{self.config._path}"',
fg="green",
)
elif source == "deezer":
secho(
click.secho(
"If you're not sure how to find the ARL cookie, see the instructions at ",
italic=True,
nl=False,
dim=True,
)
secho(
click.secho(
"https://github.com/nathom/streamrip/wiki/Finding-your-Deezer-ARL-Cookie",
underline=True,
italic=True,
fg="blue",
)
self.config.file["deezer"]["arl"] = input(style("ARL: ", fg="green"))
self.config.file["deezer"]["arl"] = input(click.style("ARL: ", fg="green"))
self.config.save()
secho(
click.secho(
f'Credentials saved to config file at "{self.config._path}"',
fg="green",
)
@ -854,19 +856,19 @@ class RipCore(list):
raise Exception
def _config_updating_message(self):
secho(
click.secho(
"Updating config file... Some settings may be lost. Please run the "
"command again.",
fg="magenta",
)
def _config_corrupted_message(self, err: Exception):
secho(
click.secho(
"There was a problem with your config file. This happens "
"sometimes after updates. Run ",
nl=False,
fg="red",
)
secho("rip config --reset ", fg="yellow", nl=False)
secho("to reset it. You will need to log in again.", fg="red")
secho(str(err), fg="red")
click.secho("rip config --reset ", fg="yellow", nl=False)
click.secho("to reset it. You will need to log in again.", fg="red")
click.secho(str(err), fg="red")