Use cleo for the CLI

This commit is contained in:
nathom 2021-07-30 17:33:26 -07:00
parent dfbe53674c
commit 0376c421b5
4 changed files with 456 additions and 718 deletions

View file

@ -1,443 +1,313 @@
"""The streamrip command line interface.""" import concurrent.futures
import logging import logging
import os
import threading
import click import requests
from cleo.application import Application as BaseApplication
from cleo.commands.command import Command
from cleo.formatters.style import Style
from click import launch
from streamrip import __version__ from streamrip import __version__
from .config import Config
from .core import RipCore
logging.basicConfig(level="WARNING") logging.basicConfig(level="WARNING")
logger = logging.getLogger("streamrip") logger = logging.getLogger("streamrip")
outdated = False
class SkipArg(Group):
def parse_args(self, ctx, args):
if len(args) == 0:
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)
# @option( class DownloadCommand(Command):
# "-u",
# "--urls",
# metavar="URLS",
# help="Url from Qobuz, Tidal, SoundCloud, or Deezer",
# multiple=True,
# )
@group(cls=SkipArg, invoke_without_command=True)
@option("-c", "--convert", metavar="CODEC", help="alac, mp3, flac, or ogg")
@option(
"-q",
"--quality",
metavar="INT",
help="0: < 320kbps, 1: 320 kbps, 2: 16 bit/44.1 kHz, 3: 24 bit/<=96 kHz, 4: 24 bit/<=192 kHz",
)
@option("-t", "--text", metavar="PATH", help="Download urls from a text file.")
@option("-nd", "--no-db", is_flag=True, help="Ignore the database.")
@option("--debug", is_flag=True, help="Show debugging logs.")
@argument("URLS", nargs=1)
@version_option(prog_name="rip", version=__version__)
@pass_context
def cli(ctx, **kwargs):
"""Streamrip: The all-in-one Qobuz, Tidal, SoundCloud, and Deezer music
downloader.
To get started, try:
$ rip -u https://www.deezer.com/en/album/6612814
For customization down to the details, see the config file:
$ rip config --open
\b
Repository: https://github.com/nathom/streamrip
Bug Reports & Feature Requests: https://github.com/nathom/streamrip/issues
Release Notes: https://github.com/nathom/streamrip/releases
""" """
import os Download items using urls.
import requests url
{--f|file=None : Path to a text file containing urls}
{--c|codec=None : Convert the downloaded files to <cmd>ALAC</cmd>, <cmd>FLAC</cmd>, <cmd>MP3</cmd>, <cmd>AAC</cmd>, or <cmd>OGG</cmd>}
{--m|max-quality=None : The maximum quality to download. Can be <cmd>0</cmd>, <cmd>1</cmd>, <cmd>2</cmd>, <cmd>3 </cmd>or <cmd>4</cmd>}
{--i|ignore-db : Download items even if they have been logged in the database.}
{urls?* : One or more Qobuz, Tidal, Deezer, or SoundCloud urls}
"""
from .config import Config help = (
from .constants import CONFIG_DIR "\nDownload <title>Dreams</title> by <title>Fleetwood Mac</title>:\n"
from .core import RipCore "$ <cmd>rip url https://www.deezer.com/en/track/63480987</cmd>\n\n"
"Batch download urls from a text file named <path>urls.txt</path>:\n"
"$ <cmd>rip url --file urls.txt</cmd>\n\n"
"For more information on Quality IDs, see\n"
"<url>https://github.com/nathom/streamrip/wiki/Quality-IDs</url>\n"
)
print(kwargs) def handle(self):
if not os.path.isdir(CONFIG_DIR): global outdated
os.makedirs(CONFIG_DIR, exist_ok=True)
global config # Use a thread so that it doesn't slow down startup
global core update_check = threading.Thread(target=is_outdated, daemon=True)
if kwargs["debug"]:
logger.setLevel("DEBUG")
logger.debug("Starting debug log")
if ctx.invoked_subcommand is None and not ctx.params["urls"]:
print(dir(cli))
echo(cli.get_help(ctx))
if ctx.invoked_subcommand not in {
None,
"lastfm",
"search",
"discover",
"config",
"repair",
}:
return
config = Config() config = Config()
path, codec, quality, no_db = clean_options(
if ctx.invoked_subcommand == "config": self.option("file"),
return self.option("codec"),
self.option("max-quality"),
if config.session["misc"]["check_for_updates"]: self.option("ignore-db"),
r = requests.get("https://pypi.org/pypi/streamrip/json").json()
newest = r["info"]["version"]
if __version__ != newest:
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")
if kwargs["no_db"]: if no_db:
config.session["database"]["enabled"] = False config.session["database"]["enabled"] = False
if kwargs["convert"]: if quality is not None:
config.session["conversion"]["enabled"] = True for source in ("qobuz", "tidal", "deezer"):
config.session["conversion"]["codec"] = kwargs["convert"] config.session[source]["quality"] = quality
if kwargs["quality"] is not None:
quality = int(kwargs["quality"])
if quality not in range(5):
secho("Invalid quality", fg="red")
return
config.session["qobuz"]["quality"] = quality
config.session["tidal"]["quality"] = quality
config.session["deezer"]["quality"] = quality
core = RipCore(config) core = RipCore(config)
if kwargs["urls"]: urls = self.argument("urls")
logger.debug(f"handling {kwargs['urls']}")
core.handle_urls(kwargs["urls"])
if kwargs["text"] is not None: if path is not None:
if os.path.isfile(kwargs["text"]): if os.path.isfile(path):
logger.debug(f"Handling {kwargs['text']}") core.handle_txt(path)
core.handle_txt(kwargs["text"])
else: else:
secho(f"Text file {kwargs['text']} does not exist.") self.line(
f"<error>File <comment>{path}</comment> does not exist.</error>"
if ctx.invoked_subcommand is None:
core.download()
@cli.command(name="filter")
@option("--repeats", is_flag=True)
@option("--non-albums", is_flag=True)
@option("--extras", is_flag=True)
@option("--features", is_flag=True)
@option("--non-studio-albums", is_flag=True)
@option("--non-remasters", is_flag=True)
@argument("URLS", nargs=-1)
@pass_context
def filter_discography(ctx, **kwargs):
"""Filter an artists discography (qobuz only).
The Qobuz API returns a massive number of tangentially related
albums when requesting an artist's discography. This command
can filter out most of the junk.
For basic filtering, use the `--repeats` and `--features` filters.
"""
raise Exception
filters = kwargs.copy()
filters.pop("urls")
config.session["filters"] = filters
logger.debug(f"downloading {kwargs['urls']} with filters {filters}")
core.handle_urls(" ".join(kwargs["urls"]))
core.download()
@cli.command()
@option(
"-t",
"--type",
default="album",
help="album, playlist, track, or artist",
show_default=True,
) )
@option( return 1
"-s",
"--source", if urls:
default="qobuz", core.handle_urls(";".join(urls))
help="qobuz, tidal, soundcloud, deezer, or deezloader",
show_default=True, if len(core) > 0:
core.download()
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>"
) )
@argument("QUERY", nargs=-1) except RuntimeError as e:
@pass_context logger.debug("Update check error: %s", e)
def search(ctx, **kwargs): pass
"""Search and download media in interactive mode.
The QUERY must be surrounded in quotes if it contains spaces. If your query return 0
contains single quotes, use double quotes, and vice versa.
Example usage:
$ rip search 'fleetwood mac rumours'
Search for a Qobuz album that matches 'fleetwood mac rumours'
$ rip search -t track 'back in the ussr'
Search for a Qobuz track with the given query
$ rip search -s tidal 'jay z 444'
Search for a Tidal album that matches 'jay z 444'
class SearchCommand(Command):
""" """
if isinstance(kwargs["query"], (list, tuple)): Search for and download items in interactive mode.
query = " ".join(kwargs["query"])
elif isinstance(kwargs["query"], str):
query = kwargs["query"]
else:
raise ValueError("Invalid query type" + type(kwargs["query"]))
if core.interactive_search(query, kwargs["source"], kwargs["type"]): search
{query : The name to search for}
{--s|source=qobuz : Qobuz, Tidal, Soundcloud, Deezer, or Deezloader}
{--t|type=album : Album, Playlist, Track, or Artist}
"""
help = (
"\nSearch for <title>Rumours</title> by <title>Fleetwood Mac</title>\n"
"$ <cmd>rip search 'rumours fleetwood mac'</cmd>\n\n"
"Search for <title>444</title> by <title>Jay-Z</title> on TIDAL\n"
"$ <cmd>rip search --source tidal '444'</cmd>\n\n"
"Search for <title>Bob Dylan</title> on Deezer\n"
"$ <cmd>rip search --type artist --source deezer 'bob dylan'</cmd>\n"
)
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() core.download()
else: else:
secho("No items chosen, exiting.", fg="bright_red") self.line("<error>No items chosen, exiting.</error>")
@cli.command() class DiscoverCommand(Command):
@option("-l", "--list", default="ideal-discography", show_default=True)
@option(
"-s", "--scrape", is_flag=True, help="Download all of the items in a list."
)
@option(
"-n", "--num-items", default=50, help="Number of items to parse.", show_default=True
)
@pass_context
def discover(ctx, **kwargs):
"""Search for albums in Qobuz's featured lists.
Avaiable options for `--list`:
\b
* most-streamed
* recent-releases
* best-sellers
* press-awards
* ideal-discography
* editor-picks
* most-featured
* qobuzissims
* new-releases
* new-releases-full
* harmonia-mundi
* universal-classic
* universal-jazz
* universal-jeunesse
* universal-chanson
""" """
Browse and download items in interactive mode (Qobuz only).
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 = (
"\nBrowse the Qobuz ideal-discography list\n"
"$ <cmd>rip discover</cmd>\n\n"
"Browse the best-sellers list\n"
"$ <cmd>rip discover best-sellers</cmd>\n\n"
"Available 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 from streamrip.constants import QOBUZ_FEATURED_KEYS
assert ( chosen_list = self.argument("list")
kwargs["list"] in QOBUZ_FEATURED_KEYS scrape = self.option("scrape")
), f"Invalid featured key {kwargs['list']}" max_items = self.option("max-items")
if kwargs["scrape"]: if chosen_list not in QOBUZ_FEATURED_KEYS:
core.scrape(kwargs["list"]) 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() core.download()
return return 0
if core.interactive_search( if core.interactive_search(
kwargs["list"], "qobuz", "featured", limit=int(kwargs["num_items"]) chosen_list, "qobuz", "featured", limit=int(max_items)
): ):
core.download() core.download()
else: else:
none_chosen() self.line("<error>No items chosen, exiting.</error>")
@cli.command() class LastfmCommand(Command):
@option(
"-s",
"--source",
help="qobuz, tidal, deezer, deezloader, or soundcloud",
)
@argument("URL")
@pass_context
def lastfm(ctx, source, url):
"""Search for tracks from a last.fm playlist on a given source.
This can be used to download playlists from Spotify and Apple Music.
To import a playlist from one of those services, go to https://www.last.fm,
log in, and to to the playlists section to import a link.
\b
Examples:
\b
Download a playlist using Qobuz as the source
$ rip lastfm https://www.last.fm/user/nathan3895/playlists/12059037
\b
Download a playlist using Tidal as the source
$ rip lastfm -s tidal https://www.last.fm/user/nathan3895/playlists/12059037
""" """
if source is not None: Search for tracks from a list.fm playlist and download them.
config.session["lastfm"]["source"] = source
core.handle_lastfm_urls(url) lastfm
{--s|source=qobuz : The source to search for items on}
{urls* : Last.fm playlist urls}
"""
help = (
"You can use this command to download Spotify, Apple Music, and YouTube "
"playlists.\nTo get started, create an account at "
"<url>https://www.last.fm</url>. Once you have\nreached the home page, "
"go to <path>Profile Icon</path> ⟶ <path>View profile</path> ⟶ "
"<path>Playlists</path> ⟶ <path>IMPORT</path>\nand paste your url.\n\n"
"Download the <info>young & free</info> Apple Music playlist (already imported)\n"
"$ <cmd>rip lastfm https://www.last.fm/user/nathan3895/playlists/12089888</cmd>\n"
)
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() core.download()
@cli.command() class ConfigCommand(Command):
@option("-o", "--open", is_flag=True, help="Open the config file") """
@option("-d", "--directory", is_flag=True, help="Open the config directory") Manage the configuration file.
@option("-q", "--qobuz", is_flag=True, help="Set Qobuz credentials")
@option("-t", "--tidal", is_flag=True, help="Re-login into Tidal") config
@option("-dz", "--deezer", is_flag=True, help="Set the Deezer ARL") {--o|open : Open the config file in the default application}
@option("--reset", is_flag=True, help="RESET the config file") {--O|open-vim : Open the config file in (neo)vim}
@option( {--d|directory : Open the directory that the config file is located in}
"--update", {--p|path : Show the config file's path}
is_flag=True, {--qobuz : Set the credentials for Qobuz}
help="Reset the config file, keeping the credentials", {--tidal : Log into Tidal}
) {--deezer : Set the Deezer ARL}
@option("-p", "--path", is_flag=True, help="Show the config file's path") {--reset : Reset the config file}
@option( {--update : Reset the config file, keeping the credentials}
"-ov", """
"--open-vim",
is_flag=True, def handle(self):
help="Open the config file in the nvim or vim text editor.",
)
@pass_context
def config(ctx, **kwargs):
"""Manage the streamrip configuration file."""
import os
import shutil import shutil
from getpass import getpass
from hashlib import md5
from streamrip.clients import TidalClient from .constants import CONFIG_DIR, CONFIG_PATH
from .constants import CONFIG_PATH config = Config()
global config if self.option("path"):
if kwargs["reset"]: self.line(f"<info>{CONFIG_PATH}</info>")
config.reset()
if kwargs["update"]: if self.option("open"):
config.update() self.line(f"Opening <url>{CONFIG_PATH}</url> in default application")
if kwargs["path"]:
echo(CONFIG_PATH)
if kwargs["open"]:
secho(f"Opening {CONFIG_PATH}", fg="green")
launch(CONFIG_PATH) launch(CONFIG_PATH)
if kwargs["open_vim"]: if self.option("reset"):
config.reset()
if self.option("update"):
config.update()
if self.option("open-vim"):
if shutil.which("nvim") is not None: if shutil.which("nvim") is not None:
os.system(f"nvim '{CONFIG_PATH}'") os.system(f"nvim '{CONFIG_PATH}'")
else: else:
os.system(f"vim '{CONFIG_PATH}'") os.system(f"vim '{CONFIG_PATH}'")
if kwargs["directory"]: if self.option("directory"):
config_dir = os.path.dirname(CONFIG_PATH) self.line(f"Opening <url>{CONFIG_DIR}</url>")
secho(f"Opening {config_dir}", fg="green") launch(CONFIG_DIR)
launch(config_dir)
if kwargs["qobuz"]: if self.option("tidal"):
config.file["qobuz"]["email"] = input(style("Qobuz email: ", fg="blue")) from streamrip.clients import TidalClient
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")
if kwargs["tidal"]:
client = TidalClient() client = TidalClient()
client.login() client.login()
config.file["tidal"].update(client.get_tokens()) config.file["tidal"].update(client.get_tokens())
config.save() config.save()
secho("Credentials saved to config.", fg="green") self.line("<info>Credentials saved to config.</info>")
if kwargs["deezer"]: if self.option("deezer"):
secho( self.line(
"If you're not sure how to find the ARL cookie, see the instructions at ", "Follow the instructions at <url>https://github.com"
italic=True, "/nathom/streamrip/wiki/Finding-your-Deezer-ARL-Cookie</url>"
nl=False,
dim=True,
) )
secho(
"https://github.com/nathom/streamrip/wiki/Finding-your-Deezer-ARL-Cookie", config.file["deezer"]["arl"] = self.ask("Paste your ARL here: ")
underline=True,
italic=True,
fg="blue",
)
config.file["deezer"]["arl"] = input(style("ARL: ", fg="green"))
config.save() config.save()
@cli.command() class ConvertCommand(Command):
@option(
"-sr", "--sampling-rate", help="Downsample the tracks to this rate, in Hz."
)
@option("-bd", "--bit-depth", help="Downsample the tracks to this bit depth.")
@option(
"-k",
"--keep-source",
is_flag=True,
help="Do not delete the old file after conversion.",
)
@argument("CODEC")
@argument("PATH")
@pass_context
def convert(ctx, **kwargs):
"""Batch convert audio files.
This is a tool that is included with the `rip` program that assists with
converting audio files. This is essentially a wrapper over ffmpeg
that is designed to be easy to use with sensible default options.
Examples (assuming /my/music is filled with FLAC files):
$ rip convert MP3 /my/music
$ rip convert ALAC --sampling-rate 48000 /my/music
""" """
import concurrent.futures A standalone tool that converts audio files to other codecs en masse.
import os
from tqdm import tqdm convert
{--s|sampling-rate=192000 : Downsample the tracks to this rate, in Hz.}
{--b|bit-depth=24 : Downsample the tracks to this bit depth.}
{--k|keep-source : Keep the original file after conversion.}
{codec : <cmd>FLAC</cmd>, <cmd>ALAC</cmd>, <cmd>OPUS</cmd>, <cmd>MP3</cmd>, or <cmd>AAC</cmd>.}
{path : The path to the audio file or a directory that contains audio files.}
"""
help = (
"\nConvert all of the audio files in <path>/my/music</path> to MP3s\n"
"$ <cmd>rip convert MP3 /my/music</cmd>\n\n"
"Downsample the audio to 48kHz after converting them to ALAC\n"
"$ <cmd>rip convert --sampling-rate 48000 ALAC /my/music\n"
)
def handle(self):
from streamrip import converter from streamrip import converter
codec_map = { CODEC_MAP = {
"FLAC": converter.FLAC, "FLAC": converter.FLAC,
"ALAC": converter.ALAC, "ALAC": converter.ALAC,
"OPUS": converter.OPUS, "OPUS": converter.OPUS,
@ -445,29 +315,36 @@ def convert(ctx, **kwargs):
"AAC": converter.AAC, "AAC": converter.AAC,
} }
codec = kwargs.get("codec").upper() codec = self.argument("codec")
assert codec in codec_map.keys(), f"Invalid codec {codec}" path = self.argument("path")
if s := kwargs.get("sampling_rate"): ConverterCls = CODEC_MAP.get(codec.upper())
sampling_rate = int(s) if ConverterCls is None:
else: self.line(
sampling_rate = None f'<error>Invalid codec "{codec}". See </error><cmd>rip convert'
" -h</cmd>."
)
return 1
if s := kwargs.get("bit_depth"): sampling_rate, bit_depth, keep_source = clean_options(
bit_depth = int(s) self.option("sampling-rate"),
else: self.option("bit-depth"),
bit_depth = None self.option("keep-source"),
)
converter_args = { converter_args = {
"sampling_rate": sampling_rate, "sampling_rate": sampling_rate,
"bit_depth": bit_depth, "bit_depth": bit_depth,
"remove_source": not kwargs.get("keep_source", False), "remove_source": not keep_source,
} }
if os.path.isdir(kwargs["path"]):
if os.path.isdir(path):
import itertools import itertools
from pathlib import Path from pathlib import Path
dirname = kwargs["path"] from tqdm import tqdm
dirname = path
audio_extensions = ("flac", "m4a", "aac", "opus", "mp3", "ogg") audio_extensions = ("flac", "m4a", "aac", "opus", "mp3", "ogg")
path_obj = Path(dirname) path_obj = Path(dirname)
audio_files = ( audio_files = (
@ -482,47 +359,122 @@ def convert(ctx, **kwargs):
for file in audio_files: for file in audio_files:
futures.append( futures.append(
executor.submit( executor.submit(
codec_map[codec]( ConverterCls(
filename=os.path.join(dirname, file), **converter_args filename=os.path.join(dirname, file), **converter_args
).convert ).convert
) )
) )
from streamrip.utils import TQDM_BAR_FORMAT
for future in tqdm( for future in tqdm(
concurrent.futures.as_completed(futures), concurrent.futures.as_completed(futures),
total=len(futures), total=len(futures),
desc="Converting", desc="Converting",
bar_format=TQDM_BAR_FORMAT,
): ):
# Only show loading bar # Only show loading bar
future.result() future.result()
elif os.path.isfile(kwargs["path"]): elif os.path.isfile(path):
codec_map[codec](filename=kwargs["path"], **converter_args).convert() ConverterCls(filename=path, **converter_args).convert()
else: else:
secho(f"File {kwargs['path']} does not exist.", fg="red") self.line(
f'<error>Path <path>"{path}"</path> does not exist.</error>', fg="red"
@cli.command()
@option(
"-n", "--num-items", help="The number of items to atttempt downloads for."
) )
@pass_context
def repair(ctx, **kwargs):
"""Retry failed downloads.
If the failed downloads database is enabled in the config file (it is by default),
when an item is not available for download, it is logged in the database.
When this command is called, it tries to download those items again. This is useful class RepairCommand(Command):
for times when a temporary server error may miss a few tracks in an album.
""" """
core.repair(max_items=kwargs.get("num_items")) Retry failed downloads.
repair
{--m|max-items=None : The maximum number of tracks to download}
"""
help = "\nRetry up to 20 failed downloads\n$ <cmd>rip repair --max-items 20</cmd>\n"
def handle(self):
max_items = clean_options(self.option("repair"))
config = Config()
RipCore(config).repair(max_items=max_items)
def none_chosen(): STRING_TO_PRIMITIVE = {
"""Print message if nothing was chosen.""" "None": None,
secho("No items chosen, exiting.", fg="bright_red") "True": True,
"False": False,
}
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)
def create_io(self, input=None, output=None, error_output=None):
io = super().create_io(input, output, error_output)
# Set our own CLI styles
formatter = io.output.formatter
formatter.set_style("url", Style("blue", options=["underline"]))
formatter.set_style("path", Style("green", options=["bold"]))
formatter.set_style("cmd", Style("magenta"))
formatter.set_style("title", Style("yellow", options=["bold"]))
io.output.set_formatter(formatter)
io.error_output.set_formatter(formatter)
self._io = io
return io
@property
def _default_definition(self):
default_globals = super()._default_definition
# as of 1.0.0a3, the descriptions don't wrap properly
# so I'm truncating the description for help as a hack
default_globals._options["help"]._description = (
default_globals._options["help"]._description.split(".")[0] + "."
)
return default_globals
def clean_options(*opts):
for opt in opts:
if isinstance(opt, str):
if opt.startswith("="):
opt = opt[1:]
opt = opt.strip()
if opt.isdigit():
opt = int(opt)
else:
opt = STRING_TO_PRIMITIVE.get(opt, opt)
yield opt
def is_outdated():
global outdated
r = requests.get("https://pypi.org/pypi/streamrip/json").json()
outdated = r["info"]["version"] != __version__
def main(): def main():
"""Run the main program.""" application = Application()
cli(obj={}) application.add(DownloadCommand())
application.add(SearchCommand())
application.add(DiscoverCommand())
application.add(LastfmCommand())
application.add(ConfigCommand())
application.add(ConvertCommand())
application.add(RepairCommand())
application.run()
if __name__ == "__main__":
main()

View file

@ -1,214 +0,0 @@
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

@ -173,10 +173,10 @@ class RipCore(list):
for source, url_type, item_id in parsed: for source, url_type, item_id in parsed:
if item_id in self.db: if item_id in self.db:
logger.info( logger.info(
f"ID {item_id} already downloaded, use --no-db to override." f"ID {item_id} already downloaded, use --ignore-db to override."
) )
secho( secho(
f"ID {item_id} already downloaded, use --no-db to override.", f"ID {item_id} already downloaded, use --ignore-db to override.",
fg="magenta", fg="magenta",
) )
continue continue

View file

@ -12,7 +12,7 @@ from pprint import pformat
from typing import Any, Dict, Generator, Optional, Sequence, Tuple, Union from typing import Any, Dict, Generator, Optional, Sequence, Tuple, Union
import deezer import deezer
from click import secho from click import launch, secho
from Cryptodome.Cipher import AES from Cryptodome.Cipher import AES
from .constants import ( from .constants import (
@ -807,7 +807,7 @@ class TidalClient(Client):
# ------------ Utilities to login ------------- # ------------ Utilities to login -------------
def _login_new_user(self, launch: bool = True): def _login_new_user(self, launch_url: bool = True):
"""Create app url where the user can log in. """Create app url where the user can log in.
:param launch: Launch the browser. :param launch: Launch the browser.
@ -819,7 +819,7 @@ class TidalClient(Client):
f"Go to {login_link} to log into Tidal within 5 minutes.", f"Go to {login_link} to log into Tidal within 5 minutes.",
fg="blue", fg="blue",
) )
if launch: if launch_url:
launch(login_link) launch(login_link)
start = time.time() start = time.time()