added interactive mode

This commit is contained in:
nathom 2021-03-22 22:27:33 -07:00
parent c79cbbd6f4
commit 5abe14aeb9
5 changed files with 77 additions and 14 deletions

View file

@ -20,6 +20,7 @@ if not os.path.isdir(CACHE_DIR):
os.makedirs(CONFIG_DIR) os.makedirs(CONFIG_DIR)
config = Config(CONFIG_PATH) config = Config(CONFIG_PATH)
core = MusicDL(config)
@click.group() @click.group()
@ -82,7 +83,6 @@ def download(ctx, **kwargs):
init_log() init_log()
config.update_from_cli(**ctx.params) config.update_from_cli(**ctx.params)
core = MusicDL(config, database=list() if kwargs["no_db"] else None)
for item in kwargs["items"]: for item in kwargs["items"]:
try: try:
if os.path.isfile(item): if os.path.isfile(item):

View file

@ -359,6 +359,7 @@ class DeezerClient(ClientInterface):
self.session = requests.Session() self.session = requests.Session()
self.logged_in = True 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: def search(self, query: str, media_type: str = "album", limit: int = 200) -> dict:
"""Search API for query. """Search API for query.

View file

@ -1,4 +1,5 @@
import os import os
from pathlib import Path
import click import click
import mutagen.id3 as id3 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) LOG_DIR = click.get_app_dir(APPNAME)
DB_PATH = os.path.join(LOG_DIR, "music-dl.db") 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" 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" TIDAL_COVER_URL = "https://resources.tidal.com/images/{uuid}/{width}x{height}.jpg"

View file

@ -2,9 +2,11 @@ import logging
import os import os
import re import re
from getpass import getpass from getpass import getpass
from string import Formatter
from typing import Generator, Optional, Tuple, Union from typing import Generator, Optional, Tuple, Union
import click import click
from simple_term_menu import TerminalMenu
from tqdm import tqdm from tqdm import tqdm
from .clients import DeezerClient, QobuzClient, TidalClient from .clients import DeezerClient, QobuzClient, TidalClient
@ -62,12 +64,12 @@ class MusicDL(list):
:type source: str :type source: str
""" """
click.secho(f"Enter {capitalize(source)} email:", fg="green") click.secho(f"Enter {capitalize(source)} email:", fg="green")
self.config[source]["email"] = input() self.config.file[source]["email"] = input()
click.secho( click.secho(
f"Enter {capitalize(source)} password (will not show on screen):", f"Enter {capitalize(source)} password (will not show on screen):",
fg="green", fg="green",
) )
self.config[source]["password"] = getpass( self.config.file[source]["password"] = getpass(
prompt="" prompt=""
) # does hashing work for tidal? ) # does hashing work for tidal?
@ -81,8 +83,8 @@ class MusicDL(list):
return return
if ( if (
self.config[source]["email"] is None self.config.file[source]["email"] is None
or self.config[source]["password"] is None or self.config.file[source]["password"] is None
): ):
self.prompt_creds(source) self.prompt_creds(source)
@ -110,8 +112,7 @@ class MusicDL(list):
"embed_cover": self.config.metadata["embed_cover"], "embed_cover": self.config.metadata["embed_cover"],
} }
client = self.clients[source] client = self.get_client(source)
self.login(client)
item = MEDIA_CLASS[media_type](client=client, id=item_id) item = MEDIA_CLASS[media_type](client=client, id=item_id)
self.append(item) self.append(item)
@ -128,6 +129,13 @@ class MusicDL(list):
click.secho(f"Downloading {item!s}", fg="bright_green") click.secho(f"Downloading {item!s}", fg="bright_green")
item.download(**arguments) 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): def convert_all(self, codec, **kwargs):
click.secho("Converting the downloaded tracks...", fg="cyan") click.secho("Converting the downloaded tracks...", fg="cyan")
for item in self: for item in self:
@ -198,14 +206,65 @@ class MusicDL(list):
self.handle_url(line) self.handle_url(line)
def search( 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: ) -> 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 if isinstance(results, Generator): # QobuzClient
for page in results: for page in results:
for item in page[f"{media_type}s"]["items"]: 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: else:
for item in results.get("data") or results.get("items"): 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]

View file

@ -692,7 +692,7 @@ class Album(Tracklist):
"title": resp.get("title"), "title": resp.get("title"),
"_artist": safe_get(resp, "artist", "name"), "_artist": safe_get(resp, "artist", "name"),
"albumartist": 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 # version not given by API
"cover_urls": { "cover_urls": {
sk: resp.get(rk) # size key, resp key sk: resp.get(rk) # size key, resp key
@ -705,7 +705,7 @@ class Album(Tracklist):
"quality": 6, # all tracks are 16/44.1 streamable "quality": 6, # all tracks are 16/44.1 streamable
"bit_depth": 16, "bit_depth": 16,
"sampling_rate": 44100, "sampling_rate": 44100,
"tracktotal": resp.get("track_total"), "tracktotal": resp.get("track_total") or resp.get("nb_tracks"),
} }
raise InvalidSourceError(client.source) raise InvalidSourceError(client.source)
@ -733,7 +733,7 @@ class Album(Tracklist):
:rtype: str :rtype: str
""" """
album_title = self._title 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(): if self.version.lower() not in album_title.lower():
album_title = f"{album_title} ({self.version})" album_title = f"{album_title} ({self.version})"