mirror of
https://github.com/nathom/streamrip.git
synced 2025-05-18 00:54:50 -04:00
Added new download function, fixed featured bugs
This commit is contained in:
parent
a46b9867b2
commit
6e93cc796c
4 changed files with 121 additions and 71 deletions
|
@ -1,13 +1,10 @@
|
|||
# For tests
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pprint import pformat
|
||||
|
||||
import click
|
||||
|
||||
from .config import Config
|
||||
from .constants import CACHE_DIR, CONFIG_DIR, CONFIG_PATH, QOBUZ_FEATURED_KEYS
|
||||
from .constants import CACHE_DIR, CONFIG_DIR, CONFIG_PATH
|
||||
from .core import MusicDL
|
||||
from .utils import init_log
|
||||
|
||||
|
@ -22,7 +19,10 @@ if not os.path.isdir(CACHE_DIR):
|
|||
|
||||
@click.group(invoke_without_command=True)
|
||||
@click.option("-c", "--convert", metavar="CODEC")
|
||||
@click.option("-u", '--urls', metavar='URLS')
|
||||
@click.option("-u", "--urls", metavar="URLS")
|
||||
@click.option("-nd", "--no-db", is_flag=True)
|
||||
@click.option("--debug", is_flag=True)
|
||||
@click.option("--reset-config", is_flag=True)
|
||||
@click.pass_context
|
||||
def cli(ctx, **kwargs):
|
||||
"""
|
||||
|
@ -56,9 +56,29 @@ def cli(ctx, **kwargs):
|
|||
global config
|
||||
global core
|
||||
|
||||
if kwargs["debug"]:
|
||||
init_log()
|
||||
|
||||
config = Config()
|
||||
if kwargs["reset_config"]:
|
||||
config.reset()
|
||||
return
|
||||
|
||||
if kwargs["no_db"]:
|
||||
config.session["database"]["enabled"] = False
|
||||
if kwargs["convert"]:
|
||||
config.session["conversion"]["enabled"] = True
|
||||
config.session["conversion"]["codec"] = kwargs["convert"]
|
||||
|
||||
core = MusicDL(config)
|
||||
|
||||
if kwargs["urls"]:
|
||||
logger.debug(f"handling {kwargs['urls']}")
|
||||
core.handle_urls(kwargs["urls"])
|
||||
|
||||
if ctx.invoked_subcommand is None:
|
||||
core.download()
|
||||
|
||||
|
||||
@cli.command(name="filter")
|
||||
@click.option("--repeats", is_flag=True)
|
||||
|
@ -79,28 +99,28 @@ def filter_discography(ctx, **kwargs):
|
|||
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}")
|
||||
filters = kwargs.copy()
|
||||
filters.remove("urls")
|
||||
config.session["filters"] = filters
|
||||
print(f"downloading {kwargs['urls']} with filters")
|
||||
logger.debug(f"downloading {kwargs['urls']} with filters {filters}")
|
||||
core.handle_urls(" ".join(kwargs["urls"]))
|
||||
core.download()
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("-t", "--type", default="album")
|
||||
@click.option("-s", "--source", default="qobuz")
|
||||
@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
|
||||
"""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
|
||||
|
@ -133,14 +153,18 @@ def interactive(ctx, **kwargs):
|
|||
|
||||
"""
|
||||
|
||||
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']}")
|
||||
logger.debug(f"starting interactive mode for type {kwargs['type']}")
|
||||
if kwargs["discover"]:
|
||||
if kwargs["query"] == ():
|
||||
kwargs["query"] = ["ideal-discography"]
|
||||
kwargs["type"] = "featured"
|
||||
|
||||
query = " ".join(kwargs["query"])
|
||||
|
||||
if core.interactive_search(query, kwargs["source"], kwargs["type"]):
|
||||
core.download()
|
||||
else:
|
||||
query = ' '.join(kwargs['query'])
|
||||
print(f"searching for query '{query}'")
|
||||
click.secho("No items chosen, exiting.", fg="bright_red")
|
||||
|
||||
|
||||
def parse_urls(arg: str):
|
||||
|
|
|
@ -40,8 +40,13 @@ class Config:
|
|||
},
|
||||
"tidal": {"enabled": True, "email": None, "password": None},
|
||||
"deezer": {"enabled": True},
|
||||
"downloads_database": None,
|
||||
"conversion": {"codec": None, "sampling_rate": None, "bit_depth": None},
|
||||
"database": {"enabled": True, "path": None},
|
||||
"conversion": {
|
||||
"enabled": False,
|
||||
"codec": None,
|
||||
"sampling_rate": None,
|
||||
"bit_depth": None,
|
||||
},
|
||||
"filters": {
|
||||
"extras": False,
|
||||
"repeats": False,
|
||||
|
@ -74,15 +79,15 @@ class Config:
|
|||
logger.debug(f"Creating yaml config file at '{self._path}'")
|
||||
self.dump(self.defaults)
|
||||
else:
|
||||
self.load_file()
|
||||
self.load()
|
||||
|
||||
def save_file(self):
|
||||
def save(self):
|
||||
self.dump(self.file)
|
||||
|
||||
def reset_file(self):
|
||||
def reset(self):
|
||||
self.dump(self.defaults)
|
||||
|
||||
def load_file(self):
|
||||
def load(self):
|
||||
with open(self._path) as cfg:
|
||||
for k, v in yaml.load(cfg).items():
|
||||
self.file[k] = v
|
||||
|
|
|
@ -2,12 +2,12 @@ import logging
|
|||
import os
|
||||
import re
|
||||
from getpass import getpass
|
||||
from pprint import pprint
|
||||
from string import Formatter
|
||||
from typing import Generator, Optional, Tuple, Union
|
||||
|
||||
import click
|
||||
from simple_term_menu import TerminalMenu
|
||||
from tqdm import tqdm
|
||||
|
||||
from .clients import DeezerClient, QobuzClient, TidalClient
|
||||
from .config import Config
|
||||
|
@ -30,16 +30,12 @@ MEDIA_CLASS = {
|
|||
CLIENTS = {"qobuz": QobuzClient, "tidal": TidalClient, "deezer": DeezerClient}
|
||||
Media = Union[Album, Playlist, Artist, Track] # type hint
|
||||
|
||||
# TODO: add support for database
|
||||
|
||||
|
||||
class MusicDL(list):
|
||||
def __init__(
|
||||
self,
|
||||
config: Optional[Config] = None,
|
||||
database: Optional[str] = None,
|
||||
):
|
||||
logger.debug(locals())
|
||||
|
||||
self.url_parse = re.compile(URL_REGEX)
|
||||
self.config = config
|
||||
|
@ -52,10 +48,15 @@ class MusicDL(list):
|
|||
"deezer": DeezerClient(),
|
||||
}
|
||||
|
||||
if isinstance(database, (MusicDB, list)):
|
||||
self.db = database
|
||||
elif database is None:
|
||||
if config.session["database"]["enabled"]:
|
||||
if config.session["database"]["path"] is not None:
|
||||
self.db = MusicDB(config.session["database"]["path"])
|
||||
else:
|
||||
self.db = MusicDB(DB_PATH)
|
||||
config.file["database"]["path"] = DB_PATH
|
||||
config.save()
|
||||
else:
|
||||
self.db = []
|
||||
|
||||
def prompt_creds(self, source: str):
|
||||
"""Prompt the user for credentials.
|
||||
|
@ -88,7 +89,7 @@ class MusicDL(list):
|
|||
):
|
||||
self.prompt_creds(source)
|
||||
|
||||
def handle_url(self, url: str):
|
||||
def handle_urls(self, url: str):
|
||||
"""Download an url
|
||||
|
||||
:param url:
|
||||
|
@ -100,34 +101,43 @@ class MusicDL(list):
|
|||
if item_id in self.db:
|
||||
logger.info(f"{url} already downloaded, use --no-db to override.")
|
||||
return
|
||||
|
||||
self.handle_item(source, url_type, item_id)
|
||||
|
||||
def handle_item(self, source: str, media_type: str, item_id: str):
|
||||
self.assert_creds(source)
|
||||
|
||||
arguments = {
|
||||
"database": self.db,
|
||||
"parent_folder": self.config.downloads["folder"],
|
||||
"quality": self.config.downloads["quality"],
|
||||
"embed_cover": self.config.metadata["embed_cover"],
|
||||
}
|
||||
|
||||
client = self.get_client(source)
|
||||
|
||||
item = MEDIA_CLASS[media_type](client=client, id=item_id)
|
||||
self.append(item)
|
||||
|
||||
def download(self):
|
||||
arguments = {
|
||||
"database": self.db,
|
||||
"parent_folder": self.config.session["downloads"]["folder"],
|
||||
"quality": self.config.session["downloads"]["quality"],
|
||||
"embed_cover": self.config.session["metadata"]["embed_cover"],
|
||||
}
|
||||
logger.debug("Arguments from config: %s", arguments)
|
||||
for item in self:
|
||||
if isinstance(item, Artist):
|
||||
keys = self.config.filters.keys()
|
||||
# TODO: move this to config.py
|
||||
filters_ = tuple(key for key in keys if self.config.filters[key])
|
||||
filters_ = tuple(
|
||||
k for k, v in self.config.session["filters"].items() if v
|
||||
)
|
||||
arguments["filters"] = filters_
|
||||
logger.debug("Added filter argument for artist/label: %s", filters_)
|
||||
|
||||
logger.debug("Arguments from config: %s", arguments)
|
||||
|
||||
item.load_meta()
|
||||
click.secho(f"Downloading {item!s}", fg="bright_green")
|
||||
item.download(**arguments)
|
||||
pprint(self.config.session["conversion"])
|
||||
if self.config.session["conversion"]["enabled"]:
|
||||
click.secho(
|
||||
f"Converting {item!s} to {self.config.session['conversion']['codec']}",
|
||||
fg="cyan",
|
||||
)
|
||||
item.convert(**self.config.session["conversion"])
|
||||
|
||||
def get_client(self, source: str):
|
||||
client = self.clients[source]
|
||||
|
@ -157,8 +167,8 @@ class MusicDL(list):
|
|||
and not creds.get("app_id")
|
||||
):
|
||||
(
|
||||
self.config["qobuz"]["app_id"],
|
||||
self.config["qobuz"]["secrets"],
|
||||
self.config.file["qobuz"]["app_id"],
|
||||
self.config.file["qobuz"]["secrets"],
|
||||
) = client.get_tokens()
|
||||
self.config.save()
|
||||
|
||||
|
@ -208,8 +218,15 @@ class MusicDL(list):
|
|||
i = 0
|
||||
if isinstance(results, Generator): # QobuzClient
|
||||
for page in results:
|
||||
for item in page[f"{media_type}s"]["items"]:
|
||||
yield MEDIA_CLASS[media_type].from_api(item, client)
|
||||
tracklist = (
|
||||
page[f"{media_type}s"]["items"]
|
||||
if media_type != "featured"
|
||||
else page["albums"]["items"]
|
||||
)
|
||||
for item in tracklist:
|
||||
yield MEDIA_CLASS[
|
||||
media_type if media_type != "featured" else "album"
|
||||
].from_api(item, client)
|
||||
i += 1
|
||||
if i > limit:
|
||||
return
|
||||
|
@ -238,7 +255,7 @@ class MusicDL(list):
|
|||
def interactive_search(
|
||||
self, query: str, source: str = "qobuz", media_type: str = "album"
|
||||
):
|
||||
results = tuple(self.search(source, query, media_type, limit=30))
|
||||
results = tuple(self.search(source, query, media_type, limit=50))
|
||||
|
||||
def title(res):
|
||||
return f"{res[0]+1}. {res[1].title}"
|
||||
|
@ -261,4 +278,8 @@ class MusicDL(list):
|
|||
clear_screen=True,
|
||||
)
|
||||
choice = menu.show()
|
||||
return results[choice]
|
||||
if choice is None:
|
||||
return False
|
||||
else:
|
||||
self.append(results[choice])
|
||||
return True
|
||||
|
|
|
@ -99,8 +99,8 @@ class Track:
|
|||
self.sampling_rate = 44100
|
||||
self.bit_depth = 16
|
||||
|
||||
self.__is_downloaded = False
|
||||
self.__is_tagged = False
|
||||
self._is_downloaded = False
|
||||
self._is_tagged = False
|
||||
for attr in ("quality", "folder", "meta"):
|
||||
setattr(self, attr, None)
|
||||
|
||||
|
@ -155,14 +155,14 @@ class Track:
|
|||
quality or self.quality,
|
||||
parent_folder or self.folder,
|
||||
)
|
||||
self.folder = sanitize_filepath(parent_folder)
|
||||
self.folder = sanitize_filepath(parent_folder, platform="auto")
|
||||
|
||||
os.makedirs(self.folder, exist_ok=True)
|
||||
|
||||
if database is not None:
|
||||
if self.id in database:
|
||||
self.__is_downloaded = True
|
||||
self.__is_tagged = True
|
||||
self._is_downloaded = True
|
||||
self._is_tagged = True
|
||||
click.secho(
|
||||
f"{self['title']} already logged in database, skipping.",
|
||||
fg="magenta",
|
||||
|
@ -170,8 +170,8 @@ class Track:
|
|||
return
|
||||
|
||||
if os.path.isfile(self.format_final_path()):
|
||||
self.__is_downloaded = True
|
||||
self.__is_tagged = True
|
||||
self._is_downloaded = True
|
||||
self._is_tagged = True
|
||||
click.secho(f"Track already downloaded: {self.final_path}", fg="magenta")
|
||||
return False
|
||||
|
||||
|
@ -195,8 +195,8 @@ class Track:
|
|||
|
||||
if os.path.isfile(temp_file):
|
||||
logger.debug("Temporary file found: %s", temp_file)
|
||||
self.__is_downloaded = True
|
||||
self.__is_tagged = False
|
||||
self._is_downloaded = True
|
||||
self._is_tagged = False
|
||||
|
||||
click.secho(f"\nDownloading {self!s}", fg="blue")
|
||||
|
||||
|
@ -217,7 +217,7 @@ class Track:
|
|||
|
||||
logger.debug("Downloaded: %s -> %s", temp_file, self.final_path)
|
||||
|
||||
self.__is_downloaded = True
|
||||
self._is_downloaded = True
|
||||
return True
|
||||
|
||||
def download_cover(self):
|
||||
|
@ -310,13 +310,13 @@ class Track:
|
|||
:type cover: Union[Picture, APIC]
|
||||
"""
|
||||
assert isinstance(self.meta, TrackMetadata), "meta must be TrackMetadata"
|
||||
if not self.__is_downloaded:
|
||||
if not self._is_downloaded:
|
||||
logger.info(
|
||||
"Track %s not tagged because it was not downloaded", self["title"]
|
||||
)
|
||||
return
|
||||
|
||||
if self.__is_tagged:
|
||||
if self._is_tagged:
|
||||
logger.info(
|
||||
"Track %s not tagged because it is already tagged", self["title"]
|
||||
)
|
||||
|
@ -361,7 +361,7 @@ class Track:
|
|||
else:
|
||||
raise ValueError(f"Unknown container type: {audio}")
|
||||
|
||||
self.__is_tagged = True
|
||||
self._is_tagged = True
|
||||
|
||||
def convert(self, codec: str = "ALAC", **kwargs):
|
||||
"""Converts the track to another codec.
|
||||
|
@ -380,7 +380,7 @@ class Track:
|
|||
:type codec: str
|
||||
:param kwargs:
|
||||
"""
|
||||
assert self.__is_downloaded, "Track must be downloaded before conversion"
|
||||
assert self._is_downloaded, "Track must be downloaded before conversion"
|
||||
|
||||
CONV_CLASS = {
|
||||
"FLAC": converter.FLAC,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue