mirror of
https://github.com/nathom/streamrip.git
synced 2025-05-19 01:35:24 -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
|
# For tests
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from getpass import getpass
|
|
||||||
import os
|
import os
|
||||||
|
from getpass import getpass
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
@ -13,23 +13,25 @@ from .core import MusicDL
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
config = Config(CONFIG_PATH)
|
config = Config(CONFIG_PATH)
|
||||||
|
|
||||||
|
|
||||||
def _get_config(ctx):
|
|
||||||
print(f"{ctx.obj=}")
|
|
||||||
if not os.path.isdir(CONFIG_DIR):
|
if not os.path.isdir(CONFIG_DIR):
|
||||||
os.makedirs(CONFIG_DIR)
|
os.makedirs(CONFIG_DIR)
|
||||||
if not os.path.isdir(CACHE_DIR):
|
if not os.path.isdir(CACHE_DIR):
|
||||||
os.makedirs(CONFIG_DIR)
|
os.makedirs(CONFIG_DIR)
|
||||||
|
|
||||||
config = Config(CONFIG_PATH)
|
config = Config(CONFIG_PATH)
|
||||||
config.update_from_cli(**ctx.obj)
|
|
||||||
return config
|
|
||||||
|
def _get_config(ctx):
|
||||||
|
config.update_from_cli(**ctx.params)
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
@click.option("--debug", default=False, is_flag=True, help="Enable debug logging")
|
@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(
|
||||||
@click.option("-c", '--convert', metavar='CODEC')
|
"--flush-cache",
|
||||||
|
metavar="PATH",
|
||||||
|
help="Flush the cache before running (only for extreme cases)",
|
||||||
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def cli(ctx, **kwargs):
|
def cli(ctx, **kwargs):
|
||||||
"""cli.
|
"""cli.
|
||||||
|
@ -46,18 +48,20 @@ def cli(ctx, **kwargs):
|
||||||
|
|
||||||
> download discography with given filters
|
> download discography with given filters
|
||||||
"""
|
"""
|
||||||
print(f"{ctx=}")
|
pass
|
||||||
print(f"{kwargs=}")
|
|
||||||
|
|
||||||
|
|
||||||
@click.command(name="dl")
|
@click.command(name="dl")
|
||||||
@click.option("-q", "--quality", metavar="INT", help="Quality integer ID (5, 6, 7, 27)")
|
@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("-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("-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.argument("items", nargs=-1)
|
||||||
@click.pass_context
|
@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.
|
Download an URL, space separated URLs or a text file with URLs.
|
||||||
Mixed arguments are also supported.
|
Mixed arguments are also supported.
|
||||||
|
@ -78,10 +82,9 @@ def download(ctx, quality, folder, search, items):
|
||||||
|
|
||||||
* Tidal (album, artist, track, playlist)
|
* Tidal (album, artist, track, playlist)
|
||||||
"""
|
"""
|
||||||
ctx.ensure_object(dict)
|
|
||||||
config = _get_config(ctx)
|
config = _get_config(ctx)
|
||||||
core = MusicDL(config)
|
core = MusicDL(config, database=list() if kwargs["no_db"] else None)
|
||||||
for item in items:
|
for item in kwargs["items"]:
|
||||||
try:
|
try:
|
||||||
if os.path.isfile(item):
|
if os.path.isfile(item):
|
||||||
core.from_txt(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"
|
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.command(name="config")
|
||||||
@click.option("-q", '--qobuz', is_flag=True)
|
@click.option("-o", "--open", is_flag=True)
|
||||||
@click.option("-t", '--tidal', is_flag=True)
|
@click.option("-q", "--qobuz", is_flag=True)
|
||||||
|
@click.option("-t", "--tidal", is_flag=True)
|
||||||
def edit_config(open, qobuz, tidal):
|
def edit_config(open, qobuz, tidal):
|
||||||
if open:
|
if open:
|
||||||
# open in text editor
|
# open in text editor
|
||||||
|
@ -106,34 +116,30 @@ def edit_config(open, qobuz, tidal):
|
||||||
return
|
return
|
||||||
|
|
||||||
if qobuz:
|
if qobuz:
|
||||||
config['qobuz']['email'] = input("Qobuz email: ")
|
config["qobuz"]["email"] = input("Qobuz email: ")
|
||||||
config['qobuz']['password'] = getpass("Qobuz password: ")
|
config["qobuz"]["password"] = getpass("Qobuz password: ")
|
||||||
config.save()
|
config.save()
|
||||||
click.secho(f"Config saved at {CONFIG_PATH}", fg='green')
|
click.secho(f"Config saved at {CONFIG_PATH}", fg="green")
|
||||||
|
|
||||||
if tidal:
|
if tidal:
|
||||||
config['tidal']['email'] = input("Tidal email: ")
|
config["tidal"]["email"] = input("Tidal email: ")
|
||||||
config['tidal']['password'] = getpass("Tidal password: ")
|
config["tidal"]["password"] = getpass("Tidal password: ")
|
||||||
config.save()
|
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.command()
|
||||||
@click.option("-t", '--type', default='album',
|
@click.option(
|
||||||
help='Type to search for. Can be album, artist, playlist, track')
|
"-t",
|
||||||
|
"--type",
|
||||||
|
default="album",
|
||||||
|
help="Type to search for. Can be album, artist, playlist, track",
|
||||||
|
)
|
||||||
@click.argument("QUERY")
|
@click.argument("QUERY")
|
||||||
def search(media_type, query):
|
def search(media_type, query):
|
||||||
print(f"searching for {media_type} with {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()
|
@click.command()
|
||||||
def interactive():
|
def interactive():
|
||||||
pass
|
pass
|
||||||
|
@ -150,8 +156,10 @@ def filter(*args):
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option("--default-comment", metavar="COMMENT", help="Custom comment tag for audio files")
|
@click.option(
|
||||||
@click.option("--no-cover", help='Do not embed cover into audio file.')
|
"--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):
|
def tags(default_comment, no_cover):
|
||||||
print(f"{default_comment=}, {no_cover=}")
|
print(f"{default_comment=}, {no_cover=}")
|
||||||
|
|
||||||
|
@ -161,8 +169,7 @@ def main():
|
||||||
cli.add_command(filter)
|
cli.add_command(filter)
|
||||||
cli.add_command(tags)
|
cli.add_command(tags)
|
||||||
cli.add_command(edit_config)
|
cli.add_command(edit_config)
|
||||||
cli.add_command(convert)
|
cli()
|
||||||
cli(obj={})
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
@ -103,6 +103,8 @@ class ClientInterface(ABC):
|
||||||
|
|
||||||
|
|
||||||
class QobuzClient(ClientInterface):
|
class QobuzClient(ClientInterface):
|
||||||
|
source = "qobuz"
|
||||||
|
|
||||||
# ------- Public Methods -------------
|
# ------- Public Methods -------------
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.logged_in = False
|
self.logged_in = False
|
||||||
|
@ -193,10 +195,6 @@ class QobuzClient(ClientInterface):
|
||||||
def get_file_url(self, item_id, quality=6) -> dict:
|
def get_file_url(self, item_id, quality=6) -> dict:
|
||||||
return self._api_get_file_url(item_id, quality=quality)
|
return self._api_get_file_url(item_id, quality=quality)
|
||||||
|
|
||||||
@property
|
|
||||||
def source(self):
|
|
||||||
return "qobuz"
|
|
||||||
|
|
||||||
# ---------- Private Methods ---------------
|
# ---------- Private Methods ---------------
|
||||||
|
|
||||||
# Credit to Sorrow446 for the original methods
|
# Credit to Sorrow446 for the original methods
|
||||||
|
@ -355,6 +353,8 @@ class QobuzClient(ClientInterface):
|
||||||
|
|
||||||
|
|
||||||
class DeezerClient(ClientInterface):
|
class DeezerClient(ClientInterface):
|
||||||
|
source = "deezer"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
self.logged_in = True
|
self.logged_in = True
|
||||||
|
@ -412,12 +412,10 @@ class DeezerClient(ClientInterface):
|
||||||
logger.debug(f"Download url {url}")
|
logger.debug(f"Download url {url}")
|
||||||
return url
|
return url
|
||||||
|
|
||||||
@property
|
|
||||||
def source(self):
|
|
||||||
return "deezer"
|
|
||||||
|
|
||||||
|
|
||||||
class TidalClient(ClientInterface):
|
class TidalClient(ClientInterface):
|
||||||
|
source = "tidal"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.logged_in = False
|
self.logged_in = False
|
||||||
|
|
||||||
|
@ -468,10 +466,6 @@ class TidalClient(ClientInterface):
|
||||||
logger.debug(f"Fetching file url with quality {quality}")
|
logger.debug(f"Fetching file url with quality {quality}")
|
||||||
return self._get_file_url(meta_id, quality=min(TIDAL_MAX_Q, 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):
|
def _search(self, query, media_type="album", **kwargs):
|
||||||
params = {
|
params = {
|
||||||
"query": query,
|
"query": query,
|
||||||
|
|
|
@ -46,6 +46,7 @@ class Config:
|
||||||
self.tidal = {"enabled": True, "email": None, "password": None}
|
self.tidal = {"enabled": True, "email": None, "password": None}
|
||||||
self.deezer = {"enabled": True}
|
self.deezer = {"enabled": True}
|
||||||
self.downloads_database = None
|
self.downloads_database = None
|
||||||
|
self.conversion = {"codec": None, "sampling_rate": None, "bit_depth": None}
|
||||||
self.filters = {
|
self.filters = {
|
||||||
"no_extras": False,
|
"no_extras": False,
|
||||||
"albums_only": False,
|
"albums_only": False,
|
||||||
|
@ -55,7 +56,7 @@ class Config:
|
||||||
}
|
}
|
||||||
self.downloads = {"folder": folder, "quality": quality}
|
self.downloads = {"folder": folder, "quality": quality}
|
||||||
self.metadata = {
|
self.metadata = {
|
||||||
"embed_cover": False,
|
"embed_cover": True,
|
||||||
"large_cover": False,
|
"large_cover": False,
|
||||||
"default_comment": None,
|
"default_comment": None,
|
||||||
"remove_extra_tags": False,
|
"remove_extra_tags": False,
|
||||||
|
|
|
@ -5,26 +5,33 @@ from getpass import getpass
|
||||||
from typing import Generator, Optional, Tuple, Union
|
from typing import Generator, Optional, Tuple, Union
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
from .clients import DeezerClient, QobuzClient, TidalClient
|
from .clients import DeezerClient, QobuzClient, TidalClient
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .constants import CONFIG_PATH, DB_PATH, URL_REGEX
|
from .constants import CONFIG_PATH, DB_PATH, URL_REGEX
|
||||||
from .db import MusicDB
|
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 .exceptions import AuthenticationError, ParsingError
|
||||||
from .utils import capitalize
|
from .utils import capitalize
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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}
|
CLIENTS = {"qobuz": QobuzClient, "tidal": TidalClient, "deezer": DeezerClient}
|
||||||
Media = Union[Album, Playlist, Artist, Track] # type hint
|
Media = Union[Album, Playlist, Artist, Track] # type hint
|
||||||
|
|
||||||
# TODO: add support for database
|
# TODO: add support for database
|
||||||
|
|
||||||
|
|
||||||
class MusicDL:
|
class MusicDL(list):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config: Optional[Config] = None,
|
config: Optional[Config] = None,
|
||||||
|
@ -43,11 +50,10 @@ class MusicDL:
|
||||||
"deezer": DeezerClient(),
|
"deezer": DeezerClient(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if database is None:
|
if isinstance(database, (MusicDB, list)):
|
||||||
self.db = MusicDB(DB_PATH)
|
|
||||||
else:
|
|
||||||
assert isinstance(database, MusicDB)
|
|
||||||
self.db = database
|
self.db = database
|
||||||
|
elif database is None:
|
||||||
|
self.db = MusicDB(DB_PATH)
|
||||||
|
|
||||||
def prompt_creds(self, source: str):
|
def prompt_creds(self, source: str):
|
||||||
"""Prompt the user for credentials.
|
"""Prompt the user for credentials.
|
||||||
|
@ -105,16 +111,10 @@ class MusicDL:
|
||||||
}
|
}
|
||||||
|
|
||||||
client = self.clients[source]
|
client = self.clients[source]
|
||||||
if not client.logged_in:
|
self.login(client)
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
client.login(**self.config.creds(source))
|
|
||||||
break
|
|
||||||
except AuthenticationError:
|
|
||||||
click.secho("Invalid credentials, try again.")
|
|
||||||
self.prompt_creds(source)
|
|
||||||
|
|
||||||
item = MEDIA_CLASS[media_type](client=client, id=item_id)
|
item = MEDIA_CLASS[media_type](client=client, id=item_id)
|
||||||
|
self.append(item)
|
||||||
if isinstance(item, Artist):
|
if isinstance(item, Artist):
|
||||||
keys = self.config.filters.keys()
|
keys = self.config.filters.keys()
|
||||||
# TODO: move this to config.py
|
# TODO: move this to config.py
|
||||||
|
@ -125,8 +125,35 @@ class MusicDL:
|
||||||
logger.debug("Arguments from config: %s", arguments)
|
logger.debug("Arguments from config: %s", arguments)
|
||||||
|
|
||||||
item.load_meta()
|
item.load_meta()
|
||||||
|
click.secho(f"Downloading {item!s}")
|
||||||
item.download(**arguments)
|
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]:
|
def parse_url(self, url: str) -> Tuple[str, str]:
|
||||||
"""Returns the type of the url and the id.
|
"""Returns the type of the url and the id.
|
||||||
|
|
||||||
|
|
|
@ -159,8 +159,16 @@ class Track:
|
||||||
|
|
||||||
os.makedirs(self.folder, exist_ok=True)
|
os.makedirs(self.folder, exist_ok=True)
|
||||||
|
|
||||||
assert database is not None # remove this later
|
if database is not None:
|
||||||
if os.path.isfile(self.format_final_path()) or self.id in database:
|
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_downloaded = True
|
||||||
self.__is_tagged = True
|
self.__is_tagged = True
|
||||||
click.secho(f"Track already downloaded: {self.final_path}", fg="green")
|
click.secho(f"Track already downloaded: {self.final_path}", fg="green")
|
||||||
|
@ -189,6 +197,8 @@ class Track:
|
||||||
self.__is_downloaded = True
|
self.__is_downloaded = True
|
||||||
self.__is_tagged = False
|
self.__is_tagged = False
|
||||||
|
|
||||||
|
click.secho(f"\nDownloading {self!s}", fg="blue")
|
||||||
|
|
||||||
if self.client.source in ("qobuz", "tidal"):
|
if self.client.source in ("qobuz", "tidal"):
|
||||||
logger.debug("Downloadable URL found: %s", dl_info.get("url"))
|
logger.debug("Downloadable URL found: %s", dl_info.get("url"))
|
||||||
tqdm_download(dl_info["url"], temp_file) # downloads file
|
tqdm_download(dl_info["url"], temp_file) # downloads file
|
||||||
|
@ -199,7 +209,11 @@ class Track:
|
||||||
raise InvalidSourceError(self.client.source)
|
raise InvalidSourceError(self.client.source)
|
||||||
|
|
||||||
shutil.move(temp_file, self.final_path)
|
shutil.move(temp_file, self.final_path)
|
||||||
|
|
||||||
|
if isinstance(database, MusicDB):
|
||||||
database.add(self.id)
|
database.add(self.id)
|
||||||
|
logger.debug(f"{self.id} added to database")
|
||||||
|
|
||||||
logger.debug("Downloaded: %s -> %s", temp_file, self.final_path)
|
logger.debug("Downloaded: %s -> %s", temp_file, self.final_path)
|
||||||
|
|
||||||
self.__is_downloaded = True
|
self.__is_downloaded = True
|
||||||
|
@ -379,6 +393,8 @@ class Track:
|
||||||
}
|
}
|
||||||
|
|
||||||
self.container = codec.upper()
|
self.container = codec.upper()
|
||||||
|
if not hasattr(self, "final_path"):
|
||||||
|
self.format_final_path()
|
||||||
|
|
||||||
engine = CONV_CLASS[codec.upper()](
|
engine = CONV_CLASS[codec.upper()](
|
||||||
filename=self.final_path,
|
filename=self.final_path,
|
||||||
|
@ -426,6 +442,14 @@ class Track:
|
||||||
"""
|
"""
|
||||||
return f"<Track - {self['title']}>"
|
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):
|
class Tracklist(list, ABC):
|
||||||
"""A base class for tracklist-like objects.
|
"""A base class for tracklist-like objects.
|
||||||
|
@ -818,6 +842,14 @@ class Album(Tracklist):
|
||||||
|
|
||||||
return f"<Album: V/A - {self.title}>"
|
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):
|
class Playlist(Tracklist):
|
||||||
"""Represents a downloadable Qobuz playlist.
|
"""Represents a downloadable Qobuz playlist.
|
||||||
|
@ -990,6 +1022,14 @@ class Playlist(Tracklist):
|
||||||
"""
|
"""
|
||||||
return f"<Playlist: {self.name}>"
|
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):
|
class Artist(Tracklist):
|
||||||
"""Represents a downloadable artist.
|
"""Represents a downloadable artist.
|
||||||
|
@ -1255,6 +1295,14 @@ class Artist(Tracklist):
|
||||||
"""
|
"""
|
||||||
return f"<Artist: {self.name}>"
|
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):
|
class Label(Artist):
|
||||||
def load_meta(self):
|
def load_meta(self):
|
||||||
|
@ -1268,3 +1316,11 @@ class Label(Artist):
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Label - {self.name}>"
|
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",
|
"Operating System :: OS Independent",
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 3 - Alpha",
|
||||||
],
|
],
|
||||||
package_dir={'', 'music-dl'},
|
package_dir={"", "music-dl"},
|
||||||
packages=find_packages(where='music-dl'),
|
packages=find_packages(where="music-dl"),
|
||||||
python_requires=">=3.9",
|
python_requires=">=3.9",
|
||||||
project_urls={
|
project_urls={
|
||||||
'Bug Reports': 'https://github.com/nathom/music-dl/issues',
|
"Bug Reports": "https://github.com/nathom/music-dl/issues",
|
||||||
'Source': 'https://github.com/nathom/music-dl',
|
"Source": "https://github.com/nathom/music-dl",
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# rm -f dist/*
|
# rm -f dist/*
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue