Basic album downloading working

This commit is contained in:
nathom 2021-03-22 18:00:04 -07:00
parent b7ace1d8d0
commit 129f3d8fe6
6 changed files with 164 additions and 79 deletions

View file

@ -1,8 +1,8 @@
# For tests
import logging
from getpass import getpass
import os
from getpass import getpass
import click
@ -13,23 +13,25 @@ from .core import MusicDL
logger = logging.getLogger(__name__)
config = Config(CONFIG_PATH)
if not os.path.isdir(CONFIG_DIR):
os.makedirs(CONFIG_DIR)
if not os.path.isdir(CACHE_DIR):
os.makedirs(CONFIG_DIR)
config = Config(CONFIG_PATH)
def _get_config(ctx):
print(f"{ctx.obj=}")
if not os.path.isdir(CONFIG_DIR):
os.makedirs(CONFIG_DIR)
if not os.path.isdir(CACHE_DIR):
os.makedirs(CONFIG_DIR)
config = Config(CONFIG_PATH)
config.update_from_cli(**ctx.obj)
return config
config.update_from_cli(**ctx.params)
@click.group()
@click.option("--debug", default=False, is_flag=True, help="Enable debug logging")
@click.option("--flush-cache", metavar="PATH", help="Flush the cache before running (only for extreme cases)")
@click.option("-c", '--convert', metavar='CODEC')
@click.option(
"--flush-cache",
metavar="PATH",
help="Flush the cache before running (only for extreme cases)",
)
@click.pass_context
def cli(ctx, **kwargs):
"""cli.
@ -46,18 +48,20 @@ def cli(ctx, **kwargs):
> download discography with given filters
"""
print(f"{ctx=}")
print(f"{kwargs=}")
pass
@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("-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.argument("items", nargs=-1)
@click.pass_context
def download(ctx, quality, folder, search, items):
def download(ctx, **kwargs):
"""
Download an URL, space separated URLs or a text file with URLs.
Mixed arguments are also supported.
@ -78,10 +82,9 @@ def download(ctx, quality, folder, search, items):
* Tidal (album, artist, track, playlist)
"""
ctx.ensure_object(dict)
config = _get_config(ctx)
core = MusicDL(config)
for item in items:
core = MusicDL(config, database=list() if kwargs["no_db"] else None)
for item in kwargs["items"]:
try:
if os.path.isfile(item):
core.from_txt(item)
@ -94,11 +97,18 @@ def download(ctx, quality, folder, search, items):
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')
@click.option('-o', "--open", is_flag=True)
@click.option("-q", '--qobuz', is_flag=True)
@click.option("-t", '--tidal', is_flag=True)
@click.command(name="config")
@click.option("-o", "--open", is_flag=True)
@click.option("-q", "--qobuz", is_flag=True)
@click.option("-t", "--tidal", is_flag=True)
def edit_config(open, qobuz, tidal):
if open:
# open in text editor
@ -106,34 +116,30 @@ def edit_config(open, qobuz, tidal):
return
if qobuz:
config['qobuz']['email'] = input("Qobuz email: ")
config['qobuz']['password'] = getpass("Qobuz password: ")
config["qobuz"]["email"] = input("Qobuz email: ")
config["qobuz"]["password"] = getpass("Qobuz password: ")
config.save()
click.secho(f"Config saved at {CONFIG_PATH}", fg='green')
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["tidal"]["email"] = input("Tidal email: ")
config["tidal"]["password"] = getpass("Tidal password: ")
config.save()
click.secho(f"Config saved at {CONFIG_PATH}", fg='green')
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.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()
@click.option("-sr", '--sampling-rate')
@click.option("-bd", "--bit-depth")
@click.argument("codec")
def convert(sampling_rate, bit_depth, codec):
print(codec, sampling_rate, bit_depth)
@click.command()
def interactive():
pass
@ -150,8 +156,10 @@ def 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.')
@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=}")
@ -161,8 +169,7 @@ def main():
cli.add_command(filter)
cli.add_command(tags)
cli.add_command(edit_config)
cli.add_command(convert)
cli(obj={})
cli()
if __name__ == "__main__":

View file

@ -103,6 +103,8 @@ class ClientInterface(ABC):
class QobuzClient(ClientInterface):
source = "qobuz"
# ------- Public Methods -------------
def __init__(self):
self.logged_in = False
@ -193,10 +195,6 @@ class QobuzClient(ClientInterface):
def get_file_url(self, item_id, quality=6) -> dict:
return self._api_get_file_url(item_id, quality=quality)
@property
def source(self):
return "qobuz"
# ---------- Private Methods ---------------
# Credit to Sorrow446 for the original methods
@ -355,6 +353,8 @@ class QobuzClient(ClientInterface):
class DeezerClient(ClientInterface):
source = "deezer"
def __init__(self):
self.session = requests.Session()
self.logged_in = True
@ -412,12 +412,10 @@ class DeezerClient(ClientInterface):
logger.debug(f"Download url {url}")
return url
@property
def source(self):
return "deezer"
class TidalClient(ClientInterface):
source = "tidal"
def __init__(self):
self.logged_in = False
@ -468,10 +466,6 @@ class TidalClient(ClientInterface):
logger.debug(f"Fetching file url with quality {quality}")
return self._get_file_url(meta_id, quality=min(TIDAL_MAX_Q, quality))
@property
def source(self):
return "tidal"
def _search(self, query, media_type="album", **kwargs):
params = {
"query": query,

View file

@ -46,6 +46,7 @@ class Config:
self.tidal = {"enabled": True, "email": None, "password": None}
self.deezer = {"enabled": True}
self.downloads_database = None
self.conversion = {"codec": None, "sampling_rate": None, "bit_depth": None}
self.filters = {
"no_extras": False,
"albums_only": False,
@ -55,7 +56,7 @@ class Config:
}
self.downloads = {"folder": folder, "quality": quality}
self.metadata = {
"embed_cover": False,
"embed_cover": True,
"large_cover": False,
"default_comment": None,
"remove_extra_tags": False,

View file

@ -5,26 +5,33 @@ from getpass import getpass
from typing import Generator, Optional, Tuple, Union
import click
from tqdm import tqdm
from .clients import DeezerClient, QobuzClient, TidalClient
from .config import Config
from .constants import CONFIG_PATH, DB_PATH, URL_REGEX
from .db import MusicDB
from .downloader import Album, Artist, Playlist, Track, Label
from .downloader import Album, Artist, Label, Playlist, Track
from .exceptions import AuthenticationError, ParsingError
from .utils import capitalize
logger = logging.getLogger(__name__)
MEDIA_CLASS = {"album": Album, "playlist": Playlist, "artist": Artist, "track": Track, "label": Label}
MEDIA_CLASS = {
"album": Album,
"playlist": Playlist,
"artist": Artist,
"track": Track,
"label": Label,
}
CLIENTS = {"qobuz": QobuzClient, "tidal": TidalClient, "deezer": DeezerClient}
Media = Union[Album, Playlist, Artist, Track] # type hint
# TODO: add support for database
class MusicDL:
class MusicDL(list):
def __init__(
self,
config: Optional[Config] = None,
@ -43,11 +50,10 @@ class MusicDL:
"deezer": DeezerClient(),
}
if database is None:
self.db = MusicDB(DB_PATH)
else:
assert isinstance(database, MusicDB)
if isinstance(database, (MusicDB, list)):
self.db = database
elif database is None:
self.db = MusicDB(DB_PATH)
def prompt_creds(self, source: str):
"""Prompt the user for credentials.
@ -105,16 +111,10 @@ class MusicDL:
}
client = self.clients[source]
if not client.logged_in:
while True:
try:
client.login(**self.config.creds(source))
break
except AuthenticationError:
click.secho("Invalid credentials, try again.")
self.prompt_creds(source)
self.login(client)
item = MEDIA_CLASS[media_type](client=client, id=item_id)
self.append(item)
if isinstance(item, Artist):
keys = self.config.filters.keys()
# TODO: move this to config.py
@ -125,8 +125,35 @@ class MusicDL:
logger.debug("Arguments from config: %s", arguments)
item.load_meta()
click.secho(f"Downloading {item!s}")
item.download(**arguments)
def convert_all(self, codec, **kwargs):
click.secho("Converting the downloaded tracks...", fg="cyan")
for item in self:
item.convert(codec, **kwargs)
def login(self, client):
creds = self.config.creds(client.source)
if not client.logged_in:
while True:
try:
client.login(**creds)
break
except AuthenticationError:
click.secho("Invalid credentials, try again.")
self.prompt_creds(client.source)
if (
client.source == "qobuz"
and not creds.get("secrets")
and not creds.get("app_id")
):
(
self.config["qobuz"]["app_id"],
self.config["qobuz"]["secrets"],
) = client.get_tokens()
self.config.save()
def parse_url(self, url: str) -> Tuple[str, str]:
"""Returns the type of the url and the id.

View file

@ -159,8 +159,16 @@ class Track:
os.makedirs(self.folder, exist_ok=True)
assert database is not None # remove this later
if os.path.isfile(self.format_final_path()) or self.id in database:
if database is not None:
if self.id in database:
self.__is_downloaded = True
self.__is_tagged = True
click.secho(
f"{self['title']} already logged in database, skipping.", fg="green"
)
return
if os.path.isfile(self.format_final_path()):
self.__is_downloaded = True
self.__is_tagged = True
click.secho(f"Track already downloaded: {self.final_path}", fg="green")
@ -189,6 +197,8 @@ class Track:
self.__is_downloaded = True
self.__is_tagged = False
click.secho(f"\nDownloading {self!s}", fg="blue")
if self.client.source in ("qobuz", "tidal"):
logger.debug("Downloadable URL found: %s", dl_info.get("url"))
tqdm_download(dl_info["url"], temp_file) # downloads file
@ -199,7 +209,11 @@ class Track:
raise InvalidSourceError(self.client.source)
shutil.move(temp_file, self.final_path)
database.add(self.id)
if isinstance(database, MusicDB):
database.add(self.id)
logger.debug(f"{self.id} added to database")
logger.debug("Downloaded: %s -> %s", temp_file, self.final_path)
self.__is_downloaded = True
@ -379,6 +393,8 @@ class Track:
}
self.container = codec.upper()
if not hasattr(self, "final_path"):
self.format_final_path()
engine = CONV_CLASS[codec.upper()](
filename=self.final_path,
@ -426,6 +442,14 @@ class Track:
"""
return f"<Track - {self['title']}>"
def __str__(self) -> str:
"""Return a readable string representation of
this track.
:rtype: str
"""
return f"{self['artist']} - {self['title']}"
class Tracklist(list, ABC):
"""A base class for tracklist-like objects.
@ -818,6 +842,14 @@ class Album(Tracklist):
return f"<Album: V/A - {self.title}>"
def __str__(self) -> str:
"""Return a readable string representation of
this album.
:rtype: str
"""
return f"{self['albumartist']} - {self['title']}"
class Playlist(Tracklist):
"""Represents a downloadable Qobuz playlist.
@ -990,6 +1022,14 @@ class Playlist(Tracklist):
"""
return f"<Playlist: {self.name}>"
def __str__(self) -> str:
"""Return a readable string representation of
this track.
:rtype: str
"""
return f"{self.name} ({len(self)} tracks)"
class Artist(Tracklist):
"""Represents a downloadable artist.
@ -1255,6 +1295,14 @@ class Artist(Tracklist):
"""
return f"<Artist: {self.name}>"
def __str__(self) -> str:
"""Return a readable string representation of
this Artist.
:rtype: str
"""
return self.name
class Label(Artist):
def load_meta(self):
@ -1268,3 +1316,11 @@ class Label(Artist):
def __repr__(self):
return f"<Label - {self.name}>"
def __str__(self) -> str:
"""Return a readable string representation of
this track.
:rtype: str
"""
return self.name

View file

@ -32,13 +32,13 @@ setup(
"Operating System :: OS Independent",
"Development Status :: 3 - Alpha",
],
package_dir={'', 'music-dl'},
packages=find_packages(where='music-dl'),
package_dir={"", "music-dl"},
packages=find_packages(where="music-dl"),
python_requires=">=3.9",
project_urls={
'Bug Reports': 'https://github.com/nathom/music-dl/issues',
'Source': 'https://github.com/nathom/music-dl',
}
"Bug Reports": "https://github.com/nathom/music-dl/issues",
"Source": "https://github.com/nathom/music-dl",
},
)
# rm -f dist/*