From 4179f3bc737c852c19073a3d231b6f2934f4626d Mon Sep 17 00:00:00 2001 From: vitiko98 Date: Mon, 14 Dec 2020 19:48:15 -0400 Subject: [PATCH] Partial rewrite * Fix #26 * Fix #25 * Fix #23 (new flag: --albums-only) * Fix #6 * Add support for last.fm playlists * Update README --- README.md | 43 ++++- qobuz_dl/cli.py | 248 +++-------------------------- qobuz_dl/commands.py | 17 +- qobuz_dl/core.py | 350 +++++++++++++++++++++++++++++++++++++++++ qobuz_dl/downloader.py | 22 ++- qobuz_dl/metadata.py | 41 +++-- qobuz_dl/search.py | 49 ------ setup.py | 9 +- 8 files changed, 456 insertions(+), 323 deletions(-) create mode 100644 qobuz_dl/core.py delete mode 100644 qobuz_dl/search.py diff --git a/README.md b/README.md index 193b7cb..1a53976 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ # qobuz-dl -Seach and download Lossless and Hi-Res music from [Qobuz](https://www.qobuz.com/). +Search, discover and download Lossless and Hi-Res music from [Qobuz](https://www.qobuz.com/). ## Features * Download FLAC and MP3 files from Qobuz -* Search and download music directly from your terminal with **interactive** or **lucky** mode +* Explore and download music directly from your terminal with **interactive** or **lucky** mode * Download albums, tracks, artists, playlists and labels with **download** mode +* Download music from last.fm playlists (Spotify, Apple Music and Youtube playlists are also supported through this method) * Queue support on **interactive** mode * Support for albums with multiple discs -* Read URLs from text file +* Downloads URLs from text file * And more ## Getting started @@ -56,10 +57,22 @@ Download albums from a label and also embed cover art images into the downloaded ``` qobuz-dl dl https://play.qobuz.com/label/7526 --embed-art ``` -Download a playlist in maximum quality +Download a Qobuz playlist in maximum quality ``` qobuz-dl dl https://play.qobuz.com/playlist/5388296 -q 27 ``` +Download all the music from an artist except singles, EPs and VA releases +``` +qobuz-dl dl https://play.qobuz.com/artist/2528676 --albums-only +``` + +#### Last.fm playlists +Last.fm has a new feature for creating playlists: you can create your own based on the music you listen or you can import one from popular streaming services like Spotify, Apple Music and Youtube. Visit: `https://www.last.fm/user//playlists` (e.g. https://www.last.fm/user/vitiko98/playlists) to get started. + +Download a last.fm playlist in the maximum quality +``` +qobuz-dl dl https://www.last.fm/user/vitiko98/playlists/11887574 -q 27 +``` Run `qobuz-dl dl --help` for more info. @@ -124,7 +137,27 @@ commands: dl input mode lucky lucky mode ``` + +## Module usage +Using `qobuz-dl` as a module is really easy. Basically, the only thing you need is to initialize `QobuzDL` from `core`. + +```python +from qobuz_dl.core import QobuzDL + +email = "your@email.com" +password = "your_password" + +qobuz = QobuzDL() +qobuz.get_tokens() # get 'app_id' and 'secrets' attrs +qobuz.initialize_client(email, password, qobuz.app_id, qobuz.secrets) + +qobuz.handle_url("https://play.qobuz.com/album/va4j3hdlwaubc") +``` + +Attributes, methods and parameters have been named as self-explanatory as possible. + ## A note about Qo-DL `qobuz-dl` is inspired in the discontinued Qo-DL-Reborn. This program uses two modules from Qo-DL: `qopy` and `spoofer`, both written by Sorrow446 and DashLt. ## Disclaimer -This tool was written for educational purposes. I will not be responsible if you use this program in bad faith. By using it, you are accepting the [Qobuz API Terms of Use](https://static.qobuz.com/apps/api/QobuzAPI-TermsofUse.pdf). +* This tool was written for educational purposes. I will not be responsible if you use this program in bad faith. By using it, you are accepting the [Qobuz API Terms of Use](https://static.qobuz.com/apps/api/QobuzAPI-TermsofUse.pdf). +* `qobuz-dl` is not affiliated with Qobuz diff --git a/qobuz_dl/cli.py b/qobuz_dl/cli.py index e2cf9ac..9cf8812 100644 --- a/qobuz_dl/cli.py +++ b/qobuz_dl/cli.py @@ -1,16 +1,10 @@ -import argparse import base64 import configparser import os -import re import sys -from pick import pick -from pathvalidate import sanitize_filename - import qobuz_dl.spoofbuz as spoofbuz -from qobuz_dl import downloader, qopy -from qobuz_dl.search import Search +from qobuz_dl.core import QobuzDL from qobuz_dl.commands import qobuz_dl_args if os.name == "nt": @@ -21,8 +15,6 @@ else: CONFIG_PATH = os.path.join(OS_CONFIG, "qobuz-dl") CONFIG_FILE = os.path.join(CONFIG_PATH, "config.ini") -QUALITIES = {5: "320", 6: "LOSSLESS", 7: "24B <96KHZ", 27: "24B <196KHZ"} - def reset_config(config_file): print("Creating config file: " + config_file) @@ -43,7 +35,7 @@ def reset_config(config_file): ) or "6" ) - config["DEFAULT"]["default_limit"] = "10" + config["DEFAULT"]["default_limit"] = "20" print("Getting tokens. Please wait...") spoofer = spoofbuz.Spoofer() config["DEFAULT"]["app_id"] = str(spoofer.getAppId()) @@ -53,193 +45,9 @@ def reset_config(config_file): print("Config file updated.") -def musicDir(directory): - fix = os.path.normpath(directory) - if not os.path.isdir(fix): - print("New directory created: " + fix) - os.makedirs(fix, exist_ok=True) - return fix - - -def get_id(url): - return re.match( - r"https?://(?:w{0,3}|play|open)\.qobuz\.com/(?:(?:album|track|artist" - "|playlist|label)/|[a-z]{2}-[a-z]{2}/album/-?\w+(?:-\w+)*-?/|user/library/favorites/)(\w+)", - url, - ).group(1) - - -def processSelected(Qz, path, albums, ids, types, quality, embed_art=False): - quality = [i for i in QUALITIES.keys()][quality[1]] - for alb, id_, type_ in zip(albums, ids, types): - for al in alb: - downloader.download_id_by_type( - Qz, - id_[al[1]], - path, - quality, - True if type_[al[1]] else False, - embed_art, - ) - - -def fromUrl(Qz, id, path, quality, album=True, embed_art=False): - downloader.download_id_by_type(Qz, id, path, str(quality), album, embed_art) - - -def handle_urls(url, client, path, quality, embed_art=False): - possibles = { - "playlist": {"func": client.get_plist_meta, "iterable_key": "tracks"}, - "artist": {"func": client.get_artist_meta, "iterable_key": "albums"}, - "label": {"func": client.get_label_meta, "iterable_key": "albums"}, - "album": {"album": True, "func": None, "iterable_key": None}, - "track": {"album": False, "func": None, "iterable_key": None}, - } - try: - url_type = url.split("/")[3] - type_dict = possibles[url_type] - item_id = get_id(url) - except (KeyError, IndexError): - print('Invalid url: "{}". Use urls from https://play.qobuz.com!'.format(url)) - return - if type_dict["func"]: - content = [item for item in type_dict["func"](item_id)] - content_name = content[0]["name"] - print( - "\nDownloading all the music from {} ({})!".format(content_name, url_type) - ) - new_path = musicDir(os.path.join(path, sanitize_filename(content_name))) - items = [item[type_dict["iterable_key"]]["items"] for item in content][0] - for item in items: - fromUrl( - client, - item["id"], - new_path, - quality, - True if type_dict["iterable_key"] == "albums" else False, - embed_art, - ) - else: - fromUrl(client, item_id, path, quality, type_dict["album"], embed_art) - - -def interactive(Qz, path, limit, tracks=True, embed_art=False): - while True: - Albums, Types, IDs = [], [], [] - try: - while True: - query = input("\nEnter your search: [Ctrl + c to quit]\n- ") - print("Searching...") - if len(query.strip()) == 0: - break - start = Search(Qz, query, limit) - start.getResults(tracks) - if len(start.Total) == 0: - break - Types.append(start.Types) - IDs.append(start.IDs) - - title = ( - "Select [space] the item(s) you want to download " - "(zero or more)\nPress Ctrl + c to quit\n" - ) - Selected = pick( - start.Total, title, multiselect=True, min_selection_count=0 - ) - if len(Selected) > 0: - Albums.append(Selected) - - y_n = pick( - ["Yes", "No"], - "Items were added to queue to be downloaded. Keep searching?", - ) - if y_n[0][0] == "N": - break - else: - break - - if len(Albums) > 0: - desc = ( - "Select [intro] the quality (the quality will be automat" - "ically\ndowngraded if the selected is not found)" - ) - Qualits = ["320", "Lossless", "Hi-res =< 96kHz", "Hi-Res > 96 kHz"] - quality = pick(Qualits, desc, default_index=1) - processSelected(Qz, path, Albums, IDs, Types, quality, embed_art) - except KeyboardInterrupt: - sys.exit("\nBye") - - -def download_by_txt_file(Qz, txt_file, path, quality, embed_art=False): - with open(txt_file, "r") as txt: - try: - urls = txt.read().strip().split() - except Exception as e: - print("Invalid text file: " + str(e)) - return - print( - 'qobuz-dl will download {} urls from file: "{}"\n'.format( - len(urls), txt_file - ) - ) - for url in urls: - handle_urls(url, Qz, path, quality, embed_art) - - -def download_lucky_mode(Qz, mode, query, limit, path, quality, embed_art=False): - if len(query) < 3: - sys.exit("Your search query is too short or invalid!") - - print( - 'Searching {}s for "{}".\n' - "qobuz-dl will attempt to download the first {} results.".format( - mode, query, limit - ) - ) - - WEB_URL = "https://play.qobuz.com/" - possibles = { - "album": { - "func": Qz.search_albums, - "album": True, - "key": "albums", - }, - "artist": { - "func": Qz.search_artists, - "album": True, - "key": "artists", - }, - "track": { - "func": Qz.search_tracks, - "album": False, - "key": "tracks", - }, - "playlist": { - "func": Qz.search_playlists, - "album": False, - "key": "playlists", - }, - } - - try: - mode_dict = possibles[mode] - results = mode_dict["func"](query, limit) - iterable = results[mode_dict["key"]]["items"] - # Use handle_urls as everything is already handled there :p - urls = ["{}{}/{}".format(WEB_URL, mode, i["id"]) for i in iterable] - print("Found {} results!".format(len(urls))) - for url in urls: - handle_urls(url, Qz, path, quality, embed_art) - except (KeyError, IndexError): - sys.exit("Invalid mode: " + str(mode)) - - def main(): if not os.path.isdir(CONFIG_PATH) or not os.path.isfile(CONFIG_FILE): - try: - os.makedirs(CONFIG_PATH, exist_ok=True) - except FileExistsError: - pass + os.makedirs(CONFIG_PATH, exist_ok=True) reset_config(CONFIG_FILE) if len(sys.argv) < 2: @@ -273,44 +81,24 @@ def main(): if arguments.reset: sys.exit(reset_config(CONFIG_FILE)) - directory = musicDir(arguments.directory) + qobuz = QobuzDL( + arguments.directory, + arguments.quality, + arguments.embed_art, + ignore_singles_eps=arguments.albums_only, + ) + qobuz.initialize_client(email, password, app_id, secrets) - Qz = qopy.Client(email, password, app_id, secrets) - - try: - quality_str = QUALITIES[int(arguments.quality)] - print("Quality set: " + quality_str) - except KeyError: - sys.exit("Invalid quality!") - - if arguments.command == "fun": - sys.exit( - interactive( - Qz, - directory, - arguments.limit, - not arguments.albums_only, - arguments.embed_art, - ) - ) if arguments.command == "dl": - for url in arguments.SOURCE: - if os.path.isfile(url): - download_by_txt_file( - Qz, url, directory, arguments.quality, arguments.embed_art - ) - else: - handle_urls(url, Qz, directory, arguments.quality, arguments.embed_art) + qobuz.download_list_of_urls(arguments.SOURCE) + elif arguments.command == "lucky": + query = " ".join(arguments.QUERY) + qobuz.lucky_type = arguments.type + qobuz.lucky_limit = arguments.number + qobuz.lucky_mode(query) else: - download_lucky_mode( - Qz, - arguments.type, - " ".join(arguments.QUERY), - arguments.number, - directory, - arguments.quality, - arguments.embed_art, - ) + qobuz.interactive_limit = arguments.limit + qobuz.interactive() if __name__ == "__main__": diff --git a/qobuz_dl/commands.py b/qobuz_dl/commands.py index a9d9c90..21be048 100644 --- a/qobuz_dl/commands.py +++ b/qobuz_dl/commands.py @@ -7,18 +7,12 @@ def fun_args(subparsers, default_limit): description="Interactively search for tracks and albums.", help="interactive mode", ) - interactive.add_argument( - "-a", - "--albums-only", - action="store_true", - help="enable albums-only search", - ) interactive.add_argument( "-l", "--limit", metavar="int", default=default_limit, - help="limit of search results by type (default: 10)", + help="limit of search results (default: 20)", ) return interactive @@ -49,7 +43,7 @@ def lucky_args(subparsers): def dl_args(subparsers): download = subparsers.add_parser( "dl", - description="Download by album/track/artist/label/playlist URL.", + description="Download by album/track/artist/label/playlist/last.fm-playlist URL.", help="input mode", ) download.add_argument( @@ -82,10 +76,15 @@ def add_common_arg(custom_parser, default_folder, default_quality): "[320, LOSSLESS, 24B <96KHZ, 24B >96KHZ] (default: 6)" ), ) + custom_parser.add_argument( + "--albums-only", + action="store_true", + help=("don't download singles, EPs and VA releases"), + ) def qobuz_dl_args( - default_quality=6, default_limit=10, default_folder="Qobuz Downloads" + default_quality=6, default_limit=20, default_folder="Qobuz Downloads" ): parser = argparse.ArgumentParser( prog="qobuz-dl", diff --git a/qobuz_dl/core.py b/qobuz_dl/core.py new file mode 100644 index 0000000..4af6630 --- /dev/null +++ b/qobuz_dl/core.py @@ -0,0 +1,350 @@ +import os +import re +import string +import sys +import time + +import requests +from bs4 import BeautifulSoup as bso +from pathvalidate import sanitize_filename + +import qobuz_dl.spoofbuz as spoofbuz +from qobuz_dl import downloader, qopy + +WEB_URL = "https://play.qobuz.com/" +ARTISTS_SELECTOR = "td.chartlist-artist > a" +TITLE_SELECTOR = "td.chartlist-name > a" + + +class PartialFormatter(string.Formatter): + def __init__(self, missing="n/a", bad_fmt="n/a"): + self.missing, self.bad_fmt = missing, bad_fmt + + def get_field(self, field_name, args, kwargs): + try: + val = super(PartialFormatter, self).get_field(field_name, args, kwargs) + except (KeyError, AttributeError): + val = None, field_name + return val + + def format_field(self, value, spec): + if not value: + return self.missing + try: + return super(PartialFormatter, self).format_field(value, spec) + except ValueError: + if self.bad_fmt: + return self.bad_fmt + raise + + +class QobuzDL: + def __init__( + self, + directory="Qobuz Downloads", + quality=6, + embed_art=False, + lucky_limit=1, + lucky_type="album", + interactive_limit=20, + ignore_singles_eps=False, + ): + self.directory = self.create_dir(directory) + self.quality = quality + self.embed_art = embed_art + self.lucky_limit = lucky_limit + self.lucky_type = lucky_type + self.interactive_limit = interactive_limit + self.ignore_singles_eps = ignore_singles_eps + + def initialize_client(self, email, pwd, app_id, secrets): + self.client = qopy.Client(email, pwd, app_id, secrets) + + def get_tokens(self): + spoofer = spoofbuz.Spoofer() + self.app_id = spoofer.getAppId() + self.secrets = [ + secret for secret in spoofer.getSecrets().values() if secret + ] # avoid empty fields + + def create_dir(self, directory=None): + fix = os.path.normpath(directory) + os.makedirs(fix, exist_ok=True) + return fix + + def get_id(self, url): + return re.match( + r"https?://(?:w{0,3}|play|open)\.qobuz\.com/(?:(?:album|track|artist" + "|playlist|label)/|[a-z]{2}-[a-z]{2}/album/-?\w+(?:-\w+)*-?/|user/" + "library/favorites/)(\w+)", + url, + ).group(1) + + def download_from_id(self, item_id, album=True, alt_path=None): + downloader.download_id_by_type( + self.client, + item_id, + self.directory if not alt_path else alt_path, + str(self.quality), + album, + self.embed_art, + self.ignore_singles_eps, + ) + + def handle_url(self, url): + possibles = { + "playlist": { + "func": self.client.get_plist_meta, + "iterable_key": "tracks", + }, + "artist": { + "func": self.client.get_artist_meta, + "iterable_key": "albums", + }, + "label": { + "func": self.client.get_label_meta, + "iterable_key": "albums", + }, + "album": {"album": True, "func": None, "iterable_key": None}, + "track": {"album": False, "func": None, "iterable_key": None}, + } + try: + url_type = url.split("/")[3] + type_dict = possibles[url_type] + item_id = self.get_id(url) + except (KeyError, IndexError): + print( + 'Invalid url: "{}". Use urls from https://play.qobuz.com!'.format(url) + ) + return + if type_dict["func"]: + content = [item for item in type_dict["func"](item_id)] + content_name = content[0]["name"] + print( + "\nDownloading all the music from {} ({})!".format( + content_name, url_type + ) + ) + new_path = self.create_dir( + os.path.join(self.directory, sanitize_filename(content_name)) + ) + items = [item[type_dict["iterable_key"]]["items"] for item in content][0] + print("{} downloads in queue".format(len(items))) + for item in items: + self.download_from_id( + item["id"], + True if type_dict["iterable_key"] == "albums" else False, + new_path, + ) + else: + self.download_from_id(item_id, type_dict["album"]) + + def download_list_of_urls(self, urls): + if not urls or not isinstance(urls, list): + print("Nothing to download") + return + for url in urls: + if "last.fm" in url: + self.download_lastfm_pl(url) + else: + self.handle_url(url) + + def download_from_txt_file(self, txt_file): + with open(txt_file, "r") as txt: + try: + urls = txt.read().strip().split() + except Exception as e: + print("Invalid text file: " + str(e)) + return + print( + 'qobuz-dl will download {} urls from file: "{}"\n'.format( + len(urls), txt_file + ) + ) + self.download_list_of_urls(urls) + + def lucky_mode(self, query, download=True): + if len(query) < 3: + sys.exit("Your search query is too short or invalid!") + + print( + 'Searching {}s for "{}".\n' + "qobuz-dl will attempt to download the first {} results.".format( + self.lucky_type, query, self.lucky_limit + ) + ) + results = self.search_by_type(query, self.lucky_type, self.lucky_limit, True) + + if download: + self.download_list_of_urls(results) + + return results + + def format_duration(self, duration): + return time.strftime("%H:%M:%S", time.gmtime(duration)) + + def search_by_type(self, query, item_type, limit=10, lucky=False): + if len(query) < 3: + print("Your search query is too short or invalid!") + return + + possibles = { + "album": { + "func": self.client.search_albums, + "album": True, + "key": "albums", + "format": "{artist[name]} - {title}", + "requires_extra": True, + }, + "artist": { + "func": self.client.search_artists, + "album": True, + "key": "artists", + "format": "{name} - ({albums_count} releases)", + "requires_extra": False, + }, + "track": { + "func": self.client.search_tracks, + "album": False, + "key": "tracks", + "format": "{performer[name]} - {title}", + "requires_extra": True, + }, + "playlist": { + "func": self.client.search_playlists, + "album": False, + "key": "playlists", + "format": "{name} - ({tracks_count} releases)", + "requires_extra": False, + }, + } + + try: + mode_dict = possibles[item_type] + results = mode_dict["func"](query, limit) + iterable = results[mode_dict["key"]]["items"] + item_list = [] + for i in iterable: + fmt = PartialFormatter() + text = fmt.format(mode_dict["format"], **i) + if mode_dict["requires_extra"]: + + text = "{} - {} [{}]".format( + text, + self.format_duration(i["duration"]), + "HI-RES" if i["hires_streamable"] else "LOSSLESS", + ) + + url = "{}{}/{}".format(WEB_URL, item_type, i.get("id", "")) + item_list.append({"text": text, "url": url} if not lucky else url) + return item_list + except (KeyError, IndexError): + print("Invalid mode: " + item_type) + return + + def interactive(self, download=True): + try: + from pick import pick + except (ImportError, ModuleNotFoundError): + if os.name == "nt": + print('Please install curses with "pip3 install windows-curses"') + return + raise + + qualities = [ + {"q_string": "320", "q": 5}, + {"q_string": "Lossless", "q": 6}, + {"q_string": "Hi-res =< 96kHz", "q": 7}, + {"q_string": "Hi-Res > 96 kHz", "q": 27}, + ] + + def get_title_text(option): + return option.get("text") + + def get_quality_text(option): + return option.get("q_string") + + try: + item_types = ["Albums", "Tracks", "Artists", "Playlists"] + selected_type = pick(item_types, "I'll search for:\n[press Intro]")[0][ + :-1 + ].lower() + print("Ok, we'll search for " + selected_type + "s") + final_url_list = [] + while True: + query = input("\nEnter your search: [Ctrl + c to quit]\n- ") + print("Searching...") + options = self.search_by_type( + query, selected_type, self.interactive_limit + ) + if not options: + print("Nothing found!") + continue + title = ( + '*** RESULTS FOR "{}" ***\n\n' + "Select [space] the item(s) you want to download " + "(one or more)\nPress Ctrl + c to quit\n" + "Don't select anything to try another search".format(query.title()) + ) + selected_items = pick( + options, + title, + multiselect=True, + min_selection_count=0, + options_map_func=get_title_text, + ) + if len(selected_items) > 0: + [final_url_list.append(i[0]["url"]) for i in selected_items] + y_n = pick( + ["Yes", "No"], + "Items were added to queue to be downloaded. Keep searching?", + ) + if y_n[0][0] == "N": + break + else: + print("\nOk, try again...") + continue + if final_url_list: + desc = ( + "Select [intro] the quality (the quality will be automat" + "ically\ndowngraded if the selected is not found)" + ) + self.quality = pick( + qualities, + desc, + default_index=1, + options_map_func=get_quality_text, + )[0]["q"] + + if download: + self.download_list_of_urls(final_url_list) + + return final_url_list + except KeyboardInterrupt: + print("\nBye") + return + + def download_lastfm_pl(self, playlist_url): + # Apparently, last fm API doesn't have a playlist endpoint. If you + # find out that it has, please fix this! + r = requests.get(playlist_url) + soup = bso(r.content, "html.parser") + artists = [artist.text for artist in soup.select(ARTISTS_SELECTOR)] + titles = [title.text for title in soup.select(TITLE_SELECTOR)] + + if len(artists) == len(titles) and artists: + track_list = [ + artist + " " + title for artist, title in zip(artists, titles) + ] + + if not track_list: + print("Nothing found") + return + + pl_title = sanitize_filename(soup.select_one("h1").text) + print("Downloading playlist: " + pl_title) + self.directory = os.path.join(self.directory, pl_title) + for i in track_list: + track_url = self.search_by_type(i, "track", 1, lucky=True)[0] + if track_url: + self.handle_url(track_url) diff --git a/qobuz_dl/downloader.py b/qobuz_dl/downloader.py index d7e80c2..4472bc3 100644 --- a/qobuz_dl/downloader.py +++ b/qobuz_dl/downloader.py @@ -79,7 +79,11 @@ def get_title(item_dict): def get_extra(i, dirn, extra="cover.jpg"): - tqdm_download(i, os.path.join(dirn, extra), "Downloading " + extra.split(".")[0]) + tqdm_download( + i.replace("_600.", "_org."), + os.path.join(dirn, extra), + "Downloading " + extra.split(".")[0], + ) # Download and tag a file @@ -149,7 +153,9 @@ def download_and_tag( os.remove(filename) -def download_id_by_type(client, item_id, path, quality, album=False, embed_art=False): +def download_id_by_type( + client, item_id, path, quality, album=False, embed_art=False, albums_only=False +): """ Download and get metadata by ID and type (album or track) @@ -157,12 +163,22 @@ def download_id_by_type(client, item_id, path, quality, album=False, embed_art=F :param int item_id: Qobuz item id :param str path: The root directory where the item will be downloaded :param int quality: Audio quality (5, 6, 7, 27) - :param bool album + :param bool album: album type or not + :param embed_art album: Embed cover art into files + :param bool albums_only: Ignore Singles, EPs and VA releases """ count = 0 if album: meta = client.get_album_meta(item_id) + + if albums_only and ( + meta.get("release_type") != "album" + or meta.get("artist").get("name") == "Various Artists" + ): + print("Ignoring Single/EP/VA: " + meta.get("title", "")) + return + album_title = get_title(meta) print("\nDownloading: {}\n".format(album_title)) dirT = ( diff --git a/qobuz_dl/metadata.py b/qobuz_dl/metadata.py index e3eeffa..0e2e688 100644 --- a/qobuz_dl/metadata.py +++ b/qobuz_dl/metadata.py @@ -4,6 +4,23 @@ from mutagen.flac import FLAC, Picture from mutagen.mp3 import EasyMP3 +def get_title(track_dict): + try: + title = ( + ("{} ({})".format(track_dict["title"], track_dict["version"])) + if track_dict["version"] + else track_dict["title"] + ) + except KeyError: + title = track_dict["title"] + + # for classical works + if track_dict.get("work"): + title = "{}: {}".format(track_dict["work"], title) + + return title + + def tag_flac(filename, root_dir, final_name, d, album, istrack=True, em_image=False): """ Tag a FLAC file @@ -18,19 +35,10 @@ def tag_flac(filename, root_dir, final_name, d, album, istrack=True, em_image=Fa """ audio = FLAC(filename) - try: - audio["TITLE"] = "{} ({})".format(d["title"], d["version"]) - except KeyError: - audio["TITLE"] = d["title"] + audio["TITLE"] = get_title(d) audio["TRACKNUMBER"] = str(d["track_number"]) # TRACK NUMBER - try: - if d["work"]: # not none - audio["WORK"] = d["work"] - except (KeyError, ValueError): - pass - try: audio["COMPOSER"] = d["composer"]["name"] # COMPOSER except KeyError: @@ -88,18 +96,9 @@ def tag_mp3(filename, root_dir, final_name, d, album, istrack=True, em_image=Fal # TODO: add embedded cover art support for mp3 audio = EasyMP3(filename) - try: - audio["title"] = "{} ({})".format(d["title"], d["version"]) - except KeyError: - audio["title"] = d["title"] + audio["title"] = get_title(d) audio["tracknumber"] = str(d["track_number"]) - - try: - if d["work"]: # not none - audio["discsubtitle"] = d["work"] - except (KeyError, ValueError): - pass try: audio["composer"] = d["composer"]["name"] except KeyError: @@ -118,7 +117,7 @@ def tag_mp3(filename, root_dir, final_name, d, album, istrack=True, em_image=Fal audio["album"] = d["album"]["title"] # ALBUM TITLE audio["date"] = d["album"]["release_date_original"].split("-")[0] else: - audio["GENRE"] = ", ".join(album["genres_list"]) # GENRE + audio["genre"] = ", ".join(album["genres_list"]) # GENRE audio["albumartist"] = album["artist"]["name"] # ALBUM ARTIST audio["album"] = album["title"] # ALBUM TITLE audio["date"] = album["release_date_original"].split("-")[0] # YEAR diff --git a/qobuz_dl/search.py b/qobuz_dl/search.py deleted file mode 100644 index 113e51d..0000000 --- a/qobuz_dl/search.py +++ /dev/null @@ -1,49 +0,0 @@ -import time - - -class Search: - def __init__(self, Qz, query, limit=10): - self.Total = [] - self.IDs = [] - self.Types = [] - self.Tracks = Qz.search_tracks(query, limit)["tracks"]["items"] - self.Albums = Qz.search_albums(query, limit)["albums"]["items"] - self.Artists = Qz.search_artists(query, limit) - - def seconds(self, duration): - return time.strftime("%H:%M:%S", time.gmtime(duration)) - - def appendInfo(self, i, bool): - self.IDs.append(i["id"]) - self.Types.append(bool) - - def itResults(self, iterable): - for i in iterable: - try: - items = ( - i["artist"]["name"], - i["title"], - self.seconds(i["duration"]), - "HI-RES" if i["hires"] else "Lossless", - ) - self.Total.append("[RELEASE] {} - {} - {} [{}]".format(*items)) - self.appendInfo(i, True) - except KeyError: - try: - artist_field = i["performer"]["name"] - except KeyError: - print("Download: " + i["title"]) - artist_field = i["composer"]["name"] - items = ( - artist_field, - i["title"], - self.seconds(i["duration"]), - "HI-RES" if i["hires"] else "Lossless", - ) - self.Total.append("[TRACK] {} - {} - {} [{}]".format(*items)) - self.appendInfo(i, False) - - def getResults(self, tracks=False): - self.itResults(self.Albums) - if tracks: - self.itResults(self.Tracks) diff --git a/setup.py b/setup.py index 191b766..543a205 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,4 @@ from setuptools import setup, find_packages -import sys -import os pkg_name = "qobuz-dl" @@ -11,12 +9,11 @@ def read_file(fname): requirements = read_file("requirements.txt").strip().split() -if os.name == "nt" or "win" in sys.platform: - requirements.append("windows-curses") + setup( name=pkg_name, - version="0.5.4.2", + version="0.5.0", author="Vitiko", author_email="vhnz98@gmail.com", description="The complete Lossless and Hi-Res music downloader for Qobuz", @@ -39,6 +36,6 @@ setup( python_requires=">=3.6", ) -# python3 setup.py sdist bdist_wheel # rm -f dist/* +# python3 setup.py sdist bdist_wheel # twine upload dist/*