mirror of
https://github.com/nathom/streamrip.git
synced 2025-05-18 17:25:22 -04:00
Basic album downloading working
This commit is contained in:
parent
b7ace1d8d0
commit
129f3d8fe6
6 changed files with 164 additions and 79 deletions
|
@ -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__":
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
10
setup.py
10
setup.py
|
@ -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/*
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue