diff --git a/rip/cli.py b/rip/cli.py
index 2eae9b9..03a1b60 100644
--- a/rip/cli.py
+++ b/rip/cli.py
@@ -1,528 +1,480 @@
-"""The streamrip command line interface."""
+import concurrent.futures
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 .config import Config
+from .core import RipCore
+
logging.basicConfig(level="WARNING")
logger = logging.getLogger("streamrip")
-
-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)
+outdated = False
-# @option(
-# "-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
+class DownloadCommand(Command):
"""
- import os
+ Download items using urls.
- import requests
-
- from .config import Config
- from .constants import CONFIG_DIR
- from .core import RipCore
-
- print(kwargs)
- if not os.path.isdir(CONFIG_DIR):
- os.makedirs(CONFIG_DIR, exist_ok=True)
-
- global config
- global core
-
- 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()
-
- if ctx.invoked_subcommand == "config":
- return
-
- if config.session["misc"]["check_for_updates"]:
- 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"]:
- config.session["database"]["enabled"] = False
-
- if kwargs["convert"]:
- config.session["conversion"]["enabled"] = True
- config.session["conversion"]["codec"] = kwargs["convert"]
-
- 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)
-
- if kwargs["urls"]:
- logger.debug(f"handling {kwargs['urls']}")
- core.handle_urls(kwargs["urls"])
-
- if kwargs["text"] is not None:
- if os.path.isfile(kwargs["text"]):
- logger.debug(f"Handling {kwargs['text']}")
- core.handle_txt(kwargs["text"])
- else:
- secho(f"Text file {kwargs['text']} does not exist.")
-
- 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.
+ url
+ {--f|file=None : Path to a text file containing urls}
+ {--c|codec=None : Convert the downloaded files to ALAC, FLAC, MP3, AAC, or OGG}
+ {--m|max-quality=None : The maximum quality to download. Can be 0, 1, 2, 3 or 4}
+ {--i|ignore-db : Download items even if they have been logged in the database.}
+ {urls?* : One or more Qobuz, Tidal, Deezer, or SoundCloud urls}
"""
- 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()
+ help = (
+ "\nDownload
Dreams by Fleetwood Mac:\n"
+ "$ rip url https://www.deezer.com/en/track/63480987\n\n"
+ "Batch download urls from a text file named urls.txt:\n"
+ "$ rip url --file urls.txt\n\n"
+ "For more information on Quality IDs, see\n"
+ "https://github.com/nathom/streamrip/wiki/Quality-IDs\n"
+ )
-@cli.command()
-@option(
- "-t",
- "--type",
- default="album",
- help="album, playlist, track, or artist",
- show_default=True,
-)
-@option(
- "-s",
- "--source",
- default="qobuz",
- help="qobuz, tidal, soundcloud, deezer, or deezloader",
- show_default=True,
-)
-@argument("QUERY", nargs=-1)
-@pass_context
-def search(ctx, **kwargs):
- """Search and download media in interactive mode.
+ def handle(self):
+ global outdated
- The QUERY must be surrounded in quotes if it contains spaces. If your query
- contains single quotes, use double quotes, and vice versa.
+ # Use a thread so that it doesn't slow down startup
+ update_check = threading.Thread(target=is_outdated, daemon=True)
- 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'
-
- """
- if isinstance(kwargs["query"], (list, tuple)):
- 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"]):
- core.download()
- else:
- secho("No items chosen, exiting.", fg="bright_red")
-
-
-@cli.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
-
- """
- from streamrip.constants import QOBUZ_FEATURED_KEYS
-
- assert (
- kwargs["list"] in QOBUZ_FEATURED_KEYS
- ), f"Invalid featured key {kwargs['list']}"
-
- if kwargs["scrape"]:
- core.scrape(kwargs["list"])
- core.download()
- return
-
- if core.interactive_search(
- kwargs["list"], "qobuz", "featured", limit=int(kwargs["num_items"])
- ):
- core.download()
- else:
- none_chosen()
-
-
-@cli.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:
- config.session["lastfm"]["source"] = source
-
- core.handle_lastfm_urls(url)
- core.download()
-
-
-@cli.command()
-@option("-o", "--open", is_flag=True, help="Open the config file")
-@option("-d", "--directory", is_flag=True, help="Open the config directory")
-@option("-q", "--qobuz", is_flag=True, help="Set Qobuz credentials")
-@option("-t", "--tidal", is_flag=True, help="Re-login into Tidal")
-@option("-dz", "--deezer", is_flag=True, help="Set the Deezer ARL")
-@option("--reset", is_flag=True, help="RESET the config file")
-@option(
- "--update",
- is_flag=True,
- help="Reset the config file, keeping the credentials",
-)
-@option("-p", "--path", is_flag=True, help="Show the config file's path")
-@option(
- "-ov",
- "--open-vim",
- is_flag=True,
- 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
- from getpass import getpass
- from hashlib import md5
-
- from streamrip.clients import TidalClient
-
- from .constants import CONFIG_PATH
-
- global config
- if kwargs["reset"]:
- config.reset()
-
- if kwargs["update"]:
- config.update()
-
- if kwargs["path"]:
- echo(CONFIG_PATH)
-
- if kwargs["open"]:
- secho(f"Opening {CONFIG_PATH}", fg="green")
- launch(CONFIG_PATH)
-
- if kwargs["open_vim"]:
- if shutil.which("nvim") is not None:
- os.system(f"nvim '{CONFIG_PATH}'")
- else:
- os.system(f"vim '{CONFIG_PATH}'")
-
- if kwargs["directory"]:
- config_dir = os.path.dirname(CONFIG_PATH)
- secho(f"Opening {config_dir}", fg="green")
- launch(config_dir)
-
- if kwargs["qobuz"]:
- config.file["qobuz"]["email"] = input(style("Qobuz email: ", fg="blue"))
-
- 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.login()
- config.file["tidal"].update(client.get_tokens())
- config.save()
- secho("Credentials saved to config.", fg="green")
-
- if kwargs["deezer"]:
- secho(
- "If you're not sure how to find the ARL cookie, see the instructions at ",
- italic=True,
- nl=False,
- dim=True,
- )
- 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.save()
-
-
-@cli.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
- import os
-
- from tqdm import tqdm
-
- from streamrip import converter
-
- codec_map = {
- "FLAC": converter.FLAC,
- "ALAC": converter.ALAC,
- "OPUS": converter.OPUS,
- "MP3": converter.LAME,
- "AAC": converter.AAC,
- }
-
- codec = kwargs.get("codec").upper()
- assert codec in codec_map.keys(), f"Invalid codec {codec}"
-
- if s := kwargs.get("sampling_rate"):
- sampling_rate = int(s)
- else:
- sampling_rate = None
-
- if s := kwargs.get("bit_depth"):
- bit_depth = int(s)
- else:
- bit_depth = None
-
- converter_args = {
- "sampling_rate": sampling_rate,
- "bit_depth": bit_depth,
- "remove_source": not kwargs.get("keep_source", False),
- }
- if os.path.isdir(kwargs["path"]):
- import itertools
- from pathlib import Path
-
- dirname = kwargs["path"]
- audio_extensions = ("flac", "m4a", "aac", "opus", "mp3", "ogg")
- path_obj = Path(dirname)
- audio_files = (
- path.as_posix()
- for path in itertools.chain.from_iterable(
- (path_obj.rglob(f"*.{ext}") for ext in audio_extensions)
- )
+ config = Config()
+ path, codec, quality, no_db = clean_options(
+ self.option("file"),
+ self.option("codec"),
+ self.option("max-quality"),
+ self.option("ignore-db"),
)
- with concurrent.futures.ThreadPoolExecutor() as executor:
- futures = []
- for file in audio_files:
- futures.append(
- executor.submit(
- codec_map[codec](
- filename=os.path.join(dirname, file), **converter_args
- ).convert
- )
+ if no_db:
+ config.session["database"]["enabled"] = False
+
+ if quality is not None:
+ for source in ("qobuz", "tidal", "deezer"):
+ config.session[source]["quality"] = quality
+
+ core = RipCore(config)
+
+ urls = self.argument("urls")
+
+ if path is not None:
+ if os.path.isfile(path):
+ core.handle_txt(path)
+ else:
+ self.line(
+ f"File {path} does not exist."
)
- for future in tqdm(
- concurrent.futures.as_completed(futures),
- total=len(futures),
- desc="Converting",
- ):
- # Only show loading bar
- future.result()
+ return 1
- 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")
+ if urls:
+ core.handle_urls(";".join(urls))
+
+ if len(core) > 0:
+ core.download()
+ elif not urls and path is None:
+ self.line("Must pass arguments. See >rip url -h.")
+
+ try:
+ update_check.join()
+ if outdated:
+ self.line(
+ "A new version of streamrip is available! Run "
+ "pip3 install streamrip --upgrade to update"
+ )
+ except RuntimeError as e:
+ logger.debug("Update check error: %s", e)
+ pass
+
+ return 0
-@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
- for times when a temporary server error may miss a few tracks in an album.
+class SearchCommand(Command):
"""
- core.repair(max_items=kwargs.get("num_items"))
+ 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}
+ """
+
+ help = (
+ "\nSearch for Rumours by Fleetwood Mac\n"
+ "$ rip search 'rumours fleetwood mac'\n\n"
+ "Search for 444 by Jay-Z on TIDAL\n"
+ "$ rip search --source tidal '444'\n\n"
+ "Search for Bob Dylan on Deezer\n"
+ "$ rip search --type artist --source deezer 'bob dylan'\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()
+ else:
+ self.line("No items chosen, exiting.")
-def none_chosen():
- """Print message if nothing was chosen."""
- secho("No items chosen, exiting.", fg="bright_red")
+class DiscoverCommand(Command):
+ """
+ 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"
+ "$ rip discover\n\n"
+ "Browse the best-sellers list\n"
+ "$ rip discover best-sellers\n\n"
+ "Available options for list:\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: list "{chosen_list}" not available')
+ 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("No items chosen, exiting.")
+
+
+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}
+ """
+
+ help = (
+ "You can use this command to download Spotify, Apple Music, and YouTube "
+ "playlists.\nTo get started, create an account at "
+ "https://www.last.fm. Once you have\nreached the home page, "
+ "go to Profile Icon ⟶ View profile ⟶ "
+ "Playlists ⟶ IMPORT\nand paste your url.\n\n"
+ "Download the young & free Apple Music playlist (already imported)\n"
+ "$ rip lastfm https://www.last.fm/user/nathan3895/playlists/12089888\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()
+
+
+class ConfigCommand(Command):
+ """
+ Manage the configuration file.
+
+ config
+ {--o|open : Open the config file in the default application}
+ {--O|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}
+ {--qobuz : Set the credentials for Qobuz}
+ {--tidal : Log into Tidal}
+ {--deezer : Set the Deezer ARL}
+ {--reset : Reset the config file}
+ {--update : Reset the config file, keeping the credentials}
+ """
+
+ def handle(self):
+ import shutil
+
+ from .constants import CONFIG_DIR, CONFIG_PATH
+
+ config = Config()
+
+ if self.option("path"):
+ self.line(f"{CONFIG_PATH}")
+
+ if self.option("open"):
+ self.line(f"Opening {CONFIG_PATH} in default application")
+ launch(CONFIG_PATH)
+
+ if self.option("reset"):
+ config.reset()
+
+ if self.option("update"):
+ config.update()
+
+ if self.option("open-vim"):
+ if shutil.which("nvim") is not None:
+ os.system(f"nvim '{CONFIG_PATH}'")
+ else:
+ os.system(f"vim '{CONFIG_PATH}'")
+
+ if self.option("directory"):
+ self.line(f"Opening {CONFIG_DIR}")
+ launch(CONFIG_DIR)
+
+ if self.option("tidal"):
+ from streamrip.clients import TidalClient
+
+ client = TidalClient()
+ client.login()
+ config.file["tidal"].update(client.get_tokens())
+ config.save()
+ self.line("Credentials saved to config.")
+
+ if self.option("deezer"):
+ self.line(
+ "Follow the instructions at https://github.com"
+ "/nathom/streamrip/wiki/Finding-your-Deezer-ARL-Cookie"
+ )
+
+ config.file["deezer"]["arl"] = self.ask("Paste your ARL here: ")
+ config.save()
+
+
+class ConvertCommand(Command):
+ """
+ A standalone tool that converts audio files to other codecs en masse.
+
+ 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 : FLAC, ALAC, OPUS, MP3, or AAC.}
+ {path : The path to the audio file or a directory that contains audio files.}
+ """
+
+ help = (
+ "\nConvert all of the audio files in /my/music to MP3s\n"
+ "$ rip convert MP3 /my/music\n\n"
+ "Downsample the audio to 48kHz after converting them to ALAC\n"
+ "$ rip convert --sampling-rate 48000 ALAC /my/music\n"
+ )
+
+ def handle(self):
+ from streamrip import converter
+
+ CODEC_MAP = {
+ "FLAC": converter.FLAC,
+ "ALAC": converter.ALAC,
+ "OPUS": converter.OPUS,
+ "MP3": converter.LAME,
+ "AAC": converter.AAC,
+ }
+
+ codec = self.argument("codec")
+ path = self.argument("path")
+
+ ConverterCls = CODEC_MAP.get(codec.upper())
+ if ConverterCls is None:
+ self.line(
+ f'Invalid codec "{codec}". See rip convert'
+ " -h."
+ )
+ return 1
+
+ sampling_rate, bit_depth, keep_source = clean_options(
+ self.option("sampling-rate"),
+ self.option("bit-depth"),
+ self.option("keep-source"),
+ )
+
+ converter_args = {
+ "sampling_rate": sampling_rate,
+ "bit_depth": bit_depth,
+ "remove_source": not keep_source,
+ }
+
+ if os.path.isdir(path):
+ import itertools
+ from pathlib import Path
+
+ from tqdm import tqdm
+
+ dirname = path
+ audio_extensions = ("flac", "m4a", "aac", "opus", "mp3", "ogg")
+ path_obj = Path(dirname)
+ audio_files = (
+ path.as_posix()
+ for path in itertools.chain.from_iterable(
+ (path_obj.rglob(f"*.{ext}") for ext in audio_extensions)
+ )
+ )
+
+ with concurrent.futures.ThreadPoolExecutor() as executor:
+ futures = []
+ for file in audio_files:
+ futures.append(
+ executor.submit(
+ ConverterCls(
+ filename=os.path.join(dirname, file), **converter_args
+ ).convert
+ )
+ )
+ from streamrip.utils import TQDM_BAR_FORMAT
+
+ for future in tqdm(
+ concurrent.futures.as_completed(futures),
+ total=len(futures),
+ desc="Converting",
+ bar_format=TQDM_BAR_FORMAT,
+ ):
+ # Only show loading bar
+ future.result()
+
+ elif os.path.isfile(path):
+ ConverterCls(filename=path, **converter_args).convert()
+ else:
+ self.line(
+ f'Path "{path}" does not exist.', fg="red"
+ )
+
+
+class RepairCommand(Command):
+ """
+ Retry failed downloads.
+
+ repair
+ {--m|max-items=None : The maximum number of tracks to download}
+ """
+
+ help = "\nRetry up to 20 failed downloads\n$ rip repair --max-items 20\n"
+
+ def handle(self):
+ max_items = clean_options(self.option("repair"))
+ config = Config()
+ RipCore(config).repair(max_items=max_items)
+
+
+STRING_TO_PRIMITIVE = {
+ "None": None,
+ "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():
- """Run the main program."""
- cli(obj={})
+ application = Application()
+ 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()
diff --git a/rip/cli_cleo.py b/rip/cli_cleo.py
deleted file mode 100644
index 0e15eda..0000000
--- a/rip/cli_cleo.py
+++ /dev/null
@@ -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'
- "$ rip url https://www.deezer.com/en/track/63480987>\n\n"
- "Batch download urls from a text file named urls.txt:\n"
- "$ 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"File {path} does not exist."
- )
- return 1
-
- if urls:
- core.handle_urls(";".join(urls))
-
- if len(core) > 0:
- core.download()
- elif not urls and path == "None":
- self.line("Must pass arguments. See >rip url -h.")
-
- 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("No items chosen, exiting.")
-
-
-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 list:\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: list "{chosen_list}" not available')
- 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("No items chosen, exiting.")
-
-
-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()
diff --git a/rip/core.py b/rip/core.py
index 1a9535c..9248bc4 100644
--- a/rip/core.py
+++ b/rip/core.py
@@ -173,10 +173,10 @@ class RipCore(list):
for source, url_type, item_id in parsed:
if item_id in self.db:
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(
- f"ID {item_id} already downloaded, use --no-db to override.",
+ f"ID {item_id} already downloaded, use --ignore-db to override.",
fg="magenta",
)
continue
diff --git a/streamrip/clients.py b/streamrip/clients.py
index ebfe11d..1aa549c 100644
--- a/streamrip/clients.py
+++ b/streamrip/clients.py
@@ -12,7 +12,7 @@ from pprint import pformat
from typing import Any, Dict, Generator, Optional, Sequence, Tuple, Union
import deezer
-from click import secho
+from click import launch, secho
from Cryptodome.Cipher import AES
from .constants import (
@@ -807,7 +807,7 @@ class TidalClient(Client):
# ------------ 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.
:param launch: Launch the browser.
@@ -819,7 +819,7 @@ class TidalClient(Client):
f"Go to {login_link} to log into Tidal within 5 minutes.",
fg="blue",
)
- if launch:
+ if launch_url:
launch(login_link)
start = time.time()