Added new download function, fixed featured bugs

This commit is contained in:
nathom 2021-03-24 18:42:33 -07:00
parent a46b9867b2
commit 6e93cc796c
4 changed files with 121 additions and 71 deletions

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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,