mirror of
https://github.com/nathom/streamrip.git
synced 2025-05-18 00:54:50 -04:00
finished ‘shell’ of cli
This commit is contained in:
parent
5abe14aeb9
commit
a46b9867b2
3 changed files with 126 additions and 144 deletions
249
music_dl/cli.py
249
music_dl/cli.py
|
@ -2,12 +2,12 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from getpass import getpass
|
from pprint import pformat
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .constants import CACHE_DIR, CONFIG_DIR, CONFIG_PATH
|
from .constants import CACHE_DIR, CONFIG_DIR, CONFIG_PATH, QOBUZ_FEATURED_KEYS
|
||||||
from .core import MusicDL
|
from .core import MusicDL
|
||||||
from .utils import init_log
|
from .utils import init_log
|
||||||
|
|
||||||
|
@ -19,156 +19,143 @@ if not os.path.isdir(CONFIG_DIR):
|
||||||
if not os.path.isdir(CACHE_DIR):
|
if not os.path.isdir(CACHE_DIR):
|
||||||
os.makedirs(CONFIG_DIR)
|
os.makedirs(CONFIG_DIR)
|
||||||
|
|
||||||
config = Config(CONFIG_PATH)
|
|
||||||
core = MusicDL(config)
|
|
||||||
|
|
||||||
|
@click.group(invoke_without_command=True)
|
||||||
@click.group()
|
@click.option("-c", "--convert", metavar="CODEC")
|
||||||
@click.option(
|
@click.option("-u", '--urls', metavar='URLS')
|
||||||
"--flush-cache",
|
|
||||||
metavar="PATH",
|
|
||||||
help="Flush the cache before running (only for extreme cases)",
|
|
||||||
)
|
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def cli(ctx, **kwargs):
|
def cli(ctx, **kwargs):
|
||||||
"""cli.
|
|
||||||
|
|
||||||
$ rip www.qobuz.com/album/id1089374 convert -c ALAC -sr 48000
|
|
||||||
|
|
||||||
> download and convert to alac, downsample to 48kHz
|
|
||||||
|
|
||||||
$ rip config --read
|
|
||||||
|
|
||||||
> Config(...)
|
|
||||||
|
|
||||||
$ rip www.qobuz.com/artist/id223049 filter --studio-albums --no-repeats
|
|
||||||
|
|
||||||
> download discography with given filters
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@click.command(name="dl")
|
|
||||||
@click.option("-q", "--quality", metavar="INT", help="Quality integer ID (5, 6, 7, 27)")
|
|
||||||
@click.option("-f", "--folder", metavar="PATH", help="Custom download folder")
|
|
||||||
@click.option("-s", "--search", metavar="QUERY")
|
|
||||||
@click.option("-nd", "--no-db", is_flag=True)
|
|
||||||
@click.option("-c", "--convert", metavar="CODEC")
|
|
||||||
@click.option("-sr", "--sampling-rate", metavar="INT")
|
|
||||||
@click.option("-bd", "--bit-depth", metavar="INT")
|
|
||||||
@click.option("--debug", default=False, is_flag=True, help="Enable debug logging")
|
|
||||||
@click.argument("items", nargs=-1)
|
|
||||||
@click.pass_context
|
|
||||||
def download(ctx, **kwargs):
|
|
||||||
"""
|
|
||||||
Download an URL, space separated URLs or a text file with URLs.
|
|
||||||
Mixed arguments are also supported.
|
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
* `qobuz-dl dl https://some.url/some_type/some_id`
|
$ rip {url} --convert alac
|
||||||
|
|
||||||
* `qobuz-dl dl file_with_urls.txt`
|
Download the url and convert to alac
|
||||||
|
|
||||||
* `qobuz-dl dl URL URL URL`
|
$ rip {artist_url} -c alac filter --repeats --non-albums
|
||||||
|
|
||||||
Supported sources and their types:
|
Download a discography, filtering repeats and non-albums
|
||||||
|
|
||||||
* Deezer (album, artist, track, playlist)
|
$ rip interactive --search
|
||||||
|
|
||||||
* Qobuz (album, artist, label, track, playlist)
|
Start an interactive search session
|
||||||
|
|
||||||
|
$ rip interactive --discover
|
||||||
|
|
||||||
|
Start an interactive discover session
|
||||||
|
|
||||||
|
$ rip config --open
|
||||||
|
|
||||||
|
Open config file
|
||||||
|
|
||||||
|
$ rip config --qobuz
|
||||||
|
|
||||||
|
Set qobuz credentials
|
||||||
|
|
||||||
* Tidal (album, artist, track, playlist)
|
|
||||||
"""
|
"""
|
||||||
if kwargs.get("debug"):
|
global config
|
||||||
init_log()
|
global core
|
||||||
|
|
||||||
config.update_from_cli(**ctx.params)
|
config = Config()
|
||||||
for item in kwargs["items"]:
|
core = MusicDL(config)
|
||||||
try:
|
|
||||||
if os.path.isfile(item):
|
|
||||||
core.from_txt(item)
|
@cli.command(name="filter")
|
||||||
click.secho(f"File input found: {item}", fg="yellow")
|
@click.option("--repeats", is_flag=True)
|
||||||
|
@click.option("--non-albums", is_flag=True)
|
||||||
|
@click.option("--extras", is_flag=True)
|
||||||
|
@click.option("--features", is_flag=True)
|
||||||
|
@click.option("--non-studio-albums", is_flag=True)
|
||||||
|
@click.option("--non-remasters", is_flag=True)
|
||||||
|
@click.argument("URLS", nargs=-1)
|
||||||
|
@click.pass_context
|
||||||
|
def filter_discography(ctx, **kwargs):
|
||||||
|
"""ONLY AVAILABLE FOR QOBUZ
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
filters = [k for k, v in kwargs.items() if v]
|
||||||
|
filters.remove('urls')
|
||||||
|
print(f"loaded filters {filters}")
|
||||||
|
config.session["filters"] = filters
|
||||||
|
print(f"downloading {kwargs['urls']} with filters")
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@click.option("-t", "--type", default="album")
|
||||||
|
@click.option("-d", "--discover", is_flag=True)
|
||||||
|
@click.argument("QUERY", nargs=-1)
|
||||||
|
@click.pass_context
|
||||||
|
def interactive(ctx, **kwargs):
|
||||||
|
f"""Interactive search for a query. This will display a menu
|
||||||
|
from which you can choose an item to download.
|
||||||
|
|
||||||
|
If the source is Qobuz, you can use the `--discover` option with
|
||||||
|
one of the following queries to fetch and interactively download
|
||||||
|
the featured albums.
|
||||||
|
|
||||||
|
{pformat(QOBUZ_FEATURED_KEYS)}
|
||||||
|
|
||||||
|
* 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
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
print(f"starting interactive mode for type {kwargs['type']}")
|
||||||
|
if kwargs['discover']:
|
||||||
|
if kwargs['query'] == ():
|
||||||
|
kwargs['query'] = 'ideal-discography'
|
||||||
|
print(f"doing a discover search of type {kwargs['query']}")
|
||||||
else:
|
else:
|
||||||
core.handle_url(item)
|
query = ' '.join(kwargs['query'])
|
||||||
except Exception as error:
|
print(f"searching for query '{query}'")
|
||||||
logger.error(error, exc_info=True)
|
|
||||||
click.secho(
|
|
||||||
f"{type(error).__name__} raised processing {item}: {error}", fg="red"
|
|
||||||
)
|
|
||||||
|
|
||||||
if ctx.params["convert"] is not None:
|
|
||||||
core.convert_all(
|
|
||||||
ctx.params["convert"],
|
|
||||||
sampling_rate=ctx.params["sampling_rate"],
|
|
||||||
bit_depth=ctx.params["bit_depth"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@click.command(name="config")
|
def parse_urls(arg: str):
|
||||||
@click.option("-o", "--open", is_flag=True)
|
if os.path.isfile(arg):
|
||||||
@click.option("-q", "--qobuz", is_flag=True)
|
return arg, "txt"
|
||||||
@click.option("-t", "--tidal", is_flag=True)
|
if "http" in arg:
|
||||||
def edit_config(open, qobuz, tidal):
|
return arg, "urls"
|
||||||
if open:
|
|
||||||
# open in text editor
|
|
||||||
click.launch(CONFIG_PATH)
|
|
||||||
return
|
|
||||||
|
|
||||||
if qobuz:
|
raise ValueError(f"Invalid argument {arg}")
|
||||||
config["qobuz"]["email"] = input("Qobuz email: ")
|
|
||||||
config["qobuz"]["password"] = getpass("Qobuz password: ")
|
|
||||||
config.save()
|
|
||||||
click.secho(f"Config saved at {CONFIG_PATH}", fg="green")
|
|
||||||
|
|
||||||
if tidal:
|
|
||||||
config["tidal"]["email"] = input("Tidal email: ")
|
|
||||||
config["tidal"]["password"] = getpass("Tidal password: ")
|
|
||||||
config.save()
|
|
||||||
click.secho(f"Config saved at {CONFIG_PATH}", fg="green")
|
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@click.option(
|
|
||||||
"-t",
|
|
||||||
"--type",
|
|
||||||
default="album",
|
|
||||||
help="Type to search for. Can be album, artist, playlist, track",
|
|
||||||
)
|
|
||||||
@click.argument("QUERY")
|
|
||||||
def search(media_type, query):
|
|
||||||
print(f"searching for {media_type} with {query=}")
|
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
def interactive():
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@click.option("--no-extras", is_flag=True, help="Ignore extras")
|
|
||||||
@click.option("--no-features", is_flag=True, help="Ignore features")
|
|
||||||
@click.option("--studio-albums", is_flag=True, help="Ignore non-studio albums")
|
|
||||||
@click.option("--remaster-only", is_flag=True, help="Ignore non-remastered albums")
|
|
||||||
@click.option("--albums-only", is_flag=True, help="Ignore non-album downloads")
|
|
||||||
def filter(*args):
|
|
||||||
print(f"filter {args=}")
|
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@click.option(
|
|
||||||
"--default-comment", metavar="COMMENT", help="Custom comment tag for audio files"
|
|
||||||
)
|
|
||||||
@click.option("--no-cover", help="Do not embed cover into audio file.")
|
|
||||||
def tags(default_comment, no_cover):
|
|
||||||
print(f"{default_comment=}, {no_cover=}")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
cli.add_command(download)
|
cli.add_command(filter_discography)
|
||||||
cli.add_command(filter)
|
cli.add_command(interactive)
|
||||||
cli.add_command(tags)
|
cli(obj={})
|
||||||
cli.add_command(edit_config)
|
|
||||||
cli()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -43,11 +43,12 @@ class Config:
|
||||||
"downloads_database": None,
|
"downloads_database": None,
|
||||||
"conversion": {"codec": None, "sampling_rate": None, "bit_depth": None},
|
"conversion": {"codec": None, "sampling_rate": None, "bit_depth": None},
|
||||||
"filters": {
|
"filters": {
|
||||||
"no_extras": False,
|
"extras": False,
|
||||||
"albums_only": False,
|
"repeats": False,
|
||||||
"no_features": False,
|
"non_albums": False,
|
||||||
"studio_albums": False,
|
"features": False,
|
||||||
"remaster_only": False,
|
"non_studio_albums": False,
|
||||||
|
"non_remaster": False,
|
||||||
},
|
},
|
||||||
"downloads": {"folder": DOWNLOADS_DIR, "quality": 7},
|
"downloads": {"folder": DOWNLOADS_DIR, "quality": 7},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
|
|
|
@ -96,7 +96,7 @@ class MusicDL(list):
|
||||||
:raises InvalidSourceError
|
:raises InvalidSourceError
|
||||||
:raises ParsingError
|
:raises ParsingError
|
||||||
"""
|
"""
|
||||||
source, url_type, item_id = self.parse_url(url)
|
source, url_type, item_id = self.parse_urls(url)[0]
|
||||||
if item_id in self.db:
|
if item_id in self.db:
|
||||||
logger.info(f"{url} already downloaded, use --no-db to override.")
|
logger.info(f"{url} already downloaded, use --no-db to override.")
|
||||||
return
|
return
|
||||||
|
@ -162,7 +162,7 @@ class MusicDL(list):
|
||||||
) = client.get_tokens()
|
) = client.get_tokens()
|
||||||
self.config.save()
|
self.config.save()
|
||||||
|
|
||||||
def parse_url(self, url: str) -> Tuple[str, str]:
|
def parse_urls(self, url: str) -> Tuple[str, str]:
|
||||||
"""Returns the type of the url and the id.
|
"""Returns the type of the url and the id.
|
||||||
|
|
||||||
Compatible with urls of the form:
|
Compatible with urls of the form:
|
||||||
|
@ -176,13 +176,10 @@ class MusicDL(list):
|
||||||
|
|
||||||
:raises exceptions.ParsingError
|
:raises exceptions.ParsingError
|
||||||
"""
|
"""
|
||||||
parsed = self.url_parse.search(url)
|
parsed = self.url_parse.findall(url)
|
||||||
|
|
||||||
if parsed is not None:
|
if parsed != []:
|
||||||
parsed = parsed.groups()
|
return parsed
|
||||||
|
|
||||||
if len(parsed) == 3:
|
|
||||||
return tuple(parsed) # Convert from Seq for the sake of typing
|
|
||||||
|
|
||||||
raise ParsingError(f"Error parsing URL: `{url}`")
|
raise ParsingError(f"Error parsing URL: `{url}`")
|
||||||
|
|
||||||
|
@ -196,14 +193,11 @@ class MusicDL(list):
|
||||||
:raises exceptions.ParsingError
|
:raises exceptions.ParsingError
|
||||||
"""
|
"""
|
||||||
with open(filepath) as txt:
|
with open(filepath) as txt:
|
||||||
lines = (
|
lines = " ".join(
|
||||||
line for line in txt.readlines() if not line.strip().startswith("#")
|
line for line in txt.readlines() if not line.strip().startswith("#")
|
||||||
)
|
)
|
||||||
|
|
||||||
click.secho(f"URLs found in text file: {len(lines)}")
|
return self.parse_urls(lines)
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
self.handle_url(line)
|
|
||||||
|
|
||||||
def search(
|
def search(
|
||||||
self, source: str, query: str, media_type: str = "album", limit: int = 200
|
self, source: str, query: str, media_type: str = "album", limit: int = 200
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue