diff --git a/music_dl/cli.py b/music_dl/cli.py index 71e35e0..e64e91b 100644 --- a/music_dl/cli.py +++ b/music_dl/cli.py @@ -20,6 +20,7 @@ if not os.path.isdir(CACHE_DIR): os.makedirs(CONFIG_DIR) config = Config(CONFIG_PATH) +core = MusicDL(config) @click.group() @@ -82,7 +83,6 @@ def download(ctx, **kwargs): init_log() config.update_from_cli(**ctx.params) - core = MusicDL(config, database=list() if kwargs["no_db"] else None) for item in kwargs["items"]: try: if os.path.isfile(item): diff --git a/music_dl/clients.py b/music_dl/clients.py index 16e61ec..9ea2cba 100644 --- a/music_dl/clients.py +++ b/music_dl/clients.py @@ -359,6 +359,7 @@ class DeezerClient(ClientInterface): self.session = requests.Session() self.logged_in = True + @region.cache_on_arguments(expiration_time=RELEASE_CACHE_TIME) def search(self, query: str, media_type: str = "album", limit: int = 200) -> dict: """Search API for query. diff --git a/music_dl/constants.py b/music_dl/constants.py index 1f50c76..c9d5cca 100644 --- a/music_dl/constants.py +++ b/music_dl/constants.py @@ -1,4 +1,5 @@ import os +from pathlib import Path import click import mutagen.id3 as id3 @@ -11,6 +12,8 @@ CONFIG_PATH = os.path.join(CONFIG_DIR, "config.yaml") LOG_DIR = click.get_app_dir(APPNAME) DB_PATH = os.path.join(LOG_DIR, "music-dl.db") +DOWNLOADS_DIR = os.path.join(Path.home(), "Music Downloads") + AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0" TIDAL_COVER_URL = "https://resources.tidal.com/images/{uuid}/{width}x{height}.jpg" diff --git a/music_dl/core.py b/music_dl/core.py index 13a7d17..5a1b303 100644 --- a/music_dl/core.py +++ b/music_dl/core.py @@ -2,9 +2,11 @@ import logging import os import re from getpass import getpass +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 @@ -62,12 +64,12 @@ class MusicDL(list): :type source: str """ click.secho(f"Enter {capitalize(source)} email:", fg="green") - self.config[source]["email"] = input() + self.config.file[source]["email"] = input() click.secho( f"Enter {capitalize(source)} password (will not show on screen):", fg="green", ) - self.config[source]["password"] = getpass( + self.config.file[source]["password"] = getpass( prompt="" ) # does hashing work for tidal? @@ -81,8 +83,8 @@ class MusicDL(list): return if ( - self.config[source]["email"] is None - or self.config[source]["password"] is None + self.config.file[source]["email"] is None + or self.config.file[source]["password"] is None ): self.prompt_creds(source) @@ -110,8 +112,7 @@ class MusicDL(list): "embed_cover": self.config.metadata["embed_cover"], } - client = self.clients[source] - self.login(client) + client = self.get_client(source) item = MEDIA_CLASS[media_type](client=client, id=item_id) self.append(item) @@ -128,6 +129,13 @@ class MusicDL(list): click.secho(f"Downloading {item!s}", fg="bright_green") item.download(**arguments) + def get_client(self, source: str): + client = self.clients[source] + if not client.logged_in: + self.assert_creds(source) + self.login(client) + return client + def convert_all(self, codec, **kwargs): click.secho("Converting the downloaded tracks...", fg="cyan") for item in self: @@ -198,14 +206,65 @@ class MusicDL(list): self.handle_url(line) def search( - self, query: str, media_type: str = "album", limit: int = 200 + self, source: str, query: str, media_type: str = "album", limit: int = 200 ) -> Generator: - results = self.client.search(query, media_type, limit) + client = self.get_client(source) + results = client.search(query, media_type) + 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, self.client) + yield MEDIA_CLASS[media_type].from_api(item, client) + i += 1 + if i > limit: + return else: for item in results.get("data") or results.get("items"): - yield MEDIA_CLASS[media_type].from_api(item, self.client) + yield MEDIA_CLASS[media_type].from_api(item, client) + i += 1 + if i > limit: + return + + def preview_media(self, media): + if isinstance(media, Album): + fmt = ( + "{albumartist} - {title}\n" + "Released on {year}\n{tracktotal} tracks\n" + "{bit_depth} bit / {sampling_rate} Hz\n" + "Version: {version}" + ) + fields = (fname for _, fname, _, _ in Formatter().parse(fmt) if fname) + ret = fmt.format(**{k: media.get(k, "Unknown") for k in fields}) + else: + raise NotImplementedError + + return ret + + def interactive_search( + self, query: str, source: str = "qobuz", media_type: str = "album" + ): + results = tuple(self.search(source, query, media_type, limit=30)) + + def title(res): + return f"{res[0]+1}. {res[1].title}" + + def from_title(s): + num = [] + for char in s: + if char.isdigit(): + num.append(char) + else: + break + return self.preview_media(results[int("".join(num)) - 1]) + + menu = TerminalMenu( + map(title, enumerate(results)), + preview_command=from_title, + preview_size=0.5, + title=f"{capitalize(source)} {media_type} search", + cycle_cursor=True, + clear_screen=True, + ) + choice = menu.show() + return results[choice] diff --git a/music_dl/downloader.py b/music_dl/downloader.py index 2c4152a..d71deea 100644 --- a/music_dl/downloader.py +++ b/music_dl/downloader.py @@ -692,7 +692,7 @@ class Album(Tracklist): "title": resp.get("title"), "_artist": safe_get(resp, "artist", "name"), "albumartist": safe_get(resp, "artist", "name"), - "year": str(resp.get("year"))[:4], + "year": str(resp.get("year"))[:4] or "Unknown", # version not given by API "cover_urls": { sk: resp.get(rk) # size key, resp key @@ -705,7 +705,7 @@ class Album(Tracklist): "quality": 6, # all tracks are 16/44.1 streamable "bit_depth": 16, "sampling_rate": 44100, - "tracktotal": resp.get("track_total"), + "tracktotal": resp.get("track_total") or resp.get("nb_tracks"), } raise InvalidSourceError(client.source) @@ -733,7 +733,7 @@ class Album(Tracklist): :rtype: str """ album_title = self._title - if isinstance(self.version, str): + if hasattr(self, "version") and isinstance(self.version, str): if self.version.lower() not in album_title.lower(): album_title = f"{album_title} ({self.version})"