mirror of
https://github.com/nathom/streamrip.git
synced 2025-05-13 14:44:49 -04:00
Use cleo for the CLI
This commit is contained in:
parent
dfbe53674c
commit
0376c421b5
4 changed files with 456 additions and 718 deletions
738
rip/cli.py
738
rip/cli.py
|
@ -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()
|
||||||
|
|
214
rip/cli_cleo.py
214
rip/cli_cleo.py
|
@ -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()
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue