From 621c6097210a3433c9b2da4075f2a89a267860e2 Mon Sep 17 00:00:00 2001 From: vitiko98 Date: Wed, 9 Dec 2020 14:52:18 -0400 Subject: [PATCH] Added new modes: interactive, download and lucky Save files in separated folders when more than one disc. Fix for #13 Handle albums with the same name and different qualities. Fix for #15 Support for embedded artwork. Fix for #12 Check if the file exists before downloading New README --- .gitignore | 1 + README.md | 101 +++++++++++++++++--- qobuz_dl/cli.py | 206 ++++++++++++++++++++++++++++------------- qobuz_dl/commands.py | 105 +++++++++++++++++++++ qobuz_dl/downloader.py | 161 ++++++++++++++++++++++++-------- qobuz_dl/metadata.py | 59 ++++++++---- qobuz_dl/qopy.py | 8 +- qobuz_dl/search.py | 1 + setup.py | 5 +- 9 files changed, 513 insertions(+), 134 deletions(-) create mode 100644 qobuz_dl/commands.py diff --git a/.gitignore b/.gitignore index 3552c94..987d30f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ subtitle_search.py *.json *.txt *.db +*.sh *.txt diff --git a/README.md b/README.md index a71fa4f..947e2f5 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,12 @@ If you need help or want to report a problem, join [qobuz-dl's discord server](h ## Features * Download FLAC and MP3 files from Qobuz -* Search and download music directly from your terminal with interactive mode -* Queue support -* Input url mode with download support for albums, tracks, artists, playlists and labels +* Search and download music directly from your terminal with **interactive** or **lucky** mode +* Download albums, tracks, artists, playlists and labels with **download** mode +* Queue support on **interactive** mode +* Support for albums with multiple discs +* Read URLs from text file +* And more ## Getting started @@ -34,21 +37,93 @@ qobuz-dl.exe > If something fails, run `qobuz-dl -r` to reset your config file. +## Examples +### Interactive mode +Run interactive mode with a limit of 10 results +``` +qobuz-dl fun -l 10 +``` +Now you can search albums and tracks: +``` +Logging... +Logged: OK +Membership: Studio + + +Enter your search: [Ctrl + c to quit] +- fka twigs magdalene +``` +Everything else is interactive. Enjoy. + +Run `qobuz-dl fun --help` for more info. + +### Download mode +Download URL in 24B<96khz quality +``` +qobuz-dl dl https://play.qobuz.com/album/qxjbxh1dc3xyb -q 7 +``` +Download multiple URLs to custom directory +``` +qobuz-dl dl https://play.qobuz.com/artist/2038380 https://play.qobuz.com/album/ip8qjy1m6dakc -d "Some pop from 2020" +``` +Download multiple URLs from text file +``` +qobuz-dl dl this_txt_file_has_urls.txt +``` +Download albums from a label and also embed cover art images into the downloaded files +``` +qobuz-dl dl https://play.qobuz.com/label/7526 --embed-art +``` +Download a playlist in maximum quality +``` +qobuz-dl dl https://play.qobuz.com/playlist/5388296 -q 27 +``` + +Run `qobuz-dl dl --help` for more info. + +### Lucky mode +Download the first album result +``` +qobuz-dl lucky playboi carti die lit +``` +Download the first 5 artist results +``` +qobuz-dl lucky joy division -n 5 --type artist +``` +Download the first 3 track results in 320 quality +``` +qobuz-dl lucky eric dolphy remastered --type track -n 3 -q 5 +``` + +Run `qobuz-dl lucky --help` for more info. + +### Other +Reset your config file +``` +qobuz-dl -r +``` + ## Usage ``` -usage: qobuz-dl [-h] [-a] [-r] [-i Album/track URL] [-q int] [-l int] [-d PATH] +usage: qobuz-dl [-h] [-r] {fun,dl,lucky} ... + +The ultimate Qobuz music downloader. +See usage examples on https://github.com/vitiko98/qobuz-dl optional arguments: - -h, --help show this help message and exit - -r create/reset config file - -a enable albums-only search - -i album/track/artist/label/playlist URL run qobuz-dl on URL input mode (download by url) - -q int quality (5, 6, 7, 27) (default: 6) [320, LOSSLESS, 24B <96KHZ, 24B >96KHZ] - -l int limit of search results by type (default: 10) - -d PATH custom directory for downloads (default: 'Qobuz Downloads') + -h, --help show this help message and exit + -r, --reset create/reset config file + +commands: + run qobuz-dl --help for more info + (e.g. qobuz-dl fun --help) + + {fun,dl,lucky} + fun interactive mode + dl input mode + lucky lucky mode ``` ## 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. -Also, you are accepting this: [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). diff --git a/qobuz_dl/cli.py b/qobuz_dl/cli.py index 05cd152..024a22e 100644 --- a/qobuz_dl/cli.py +++ b/qobuz_dl/cli.py @@ -5,10 +5,12 @@ 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.commands import qobuz_dl_args if os.name == "nt": OS_CONFIG = os.environ.get("APPDATA") @@ -18,6 +20,8 @@ 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) @@ -46,40 +50,11 @@ def reset_config(config_file): print("Config file updated.") -def getArgs(default_quality=6, default_limit=10, default_folder="Qobuz Downloads"): - parser = argparse.ArgumentParser(prog="qobuz-dl") - parser.add_argument("-a", action="store_true", help="enable albums-only search") - parser.add_argument("-r", action="store_true", help="create/reset config file") - parser.add_argument( - "-i", - metavar="album/track/artist/label/playlist URL", - help="run qobuz-dl on URL input mode (download by url)", - ) - parser.add_argument( - "-q", - metavar="int", - default=default_quality, - help="quality for url input mode (5, 6, 7, 27) (default: 6)", - ) - parser.add_argument( - "-l", - metavar="int", - default=default_limit, - help="limit of search results by type (default: 10)", - ) - parser.add_argument( - "-d", - metavar="PATH", - default=default_folder, - help="custom directory for downloads (default: '{}')".format(default_folder), - ) - return parser.parse_args() - - -def musicDir(dir): - fix = os.path.normpath(dir) +def musicDir(directory): + fix = os.path.normpath(directory) if not os.path.isdir(fix): - os.mkdir(fix) + print("New directory created: " + fix) + os.makedirs(fix, exist_ok=True) return fix @@ -91,21 +66,25 @@ def get_id(url): ).group(1) -def processSelected(Qz, path, albums, ids, types, quality): - q = ["5", "6", "7", "27"] - quality = q[quality[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.iterateIDs( - Qz, id_[al[1]], path, quality, True if type_[al[1]] else False + 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): - downloader.iterateIDs(Qz, id, path, str(quality), album) +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): +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"}, @@ -117,28 +96,31 @@ def handle_urls(url, client, path, quality): url_type = url.split("/")[3] type_dict = possibles[url_type] item_id = get_id(url) - print("Downloading {}...".format(url_type)) - except KeyError: - print("Invalid url. Use urls from https://play.qobuz.com!") + except (KeyError, IndexError): + print('Invalid url: "{}". Use urls from https://play.qobuz.com!'.format(url)) return if type_dict["func"]: - items = [ - item[type_dict["iterable_key"]]["items"] - for item in type_dict["func"](item_id) - ][0] + 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"], - path, + new_path, quality, True if type_dict["iterable_key"] == "albums" else False, + embed_art, ) else: - fromUrl(client, item_id, path, quality, type_dict["album"]) + fromUrl(client, item_id, path, quality, type_dict["album"], embed_art) -def interactive(Qz, path, limit, tracks=True): +def interactive(Qz, path, limit, tracks=True, embed_art=False): while True: Albums, Types, IDs = [], [], [] try: @@ -180,19 +162,86 @@ def interactive(Qz, path, limit, tracks=True): ) Qualits = ["320", "Lossless", "Hi-res =< 96kHz", "Hi-Res > 96 kHz"] quality = pick(Qualits, desc, default_index=1) - processSelected(Qz, path, Albums, IDs, Types, quality) + 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.mkdir(CONFIG_PATH) + os.makedirs(CONFIG_PATH, exist_ok=True) except FileExistsError: pass reset_config(CONFIG_FILE) + if len(sys.argv) < 2: + sys.exit(qobuz_dl_args().print_help()) + email = None password = None app_id = None @@ -211,21 +260,54 @@ def main(): secrets = [ secret for secret in config["DEFAULT"]["secrets"].split(",") if secret ] - arguments = getArgs(default_quality, default_limit, default_folder) + arguments = qobuz_dl_args( + default_quality, default_limit, default_folder + ).parse_args() except KeyError: - print("Your config file is corrupted! Run 'qobuz-dl -r' to fix this\n") - arguments = getArgs() - - if arguments.r: + arguments = qobuz_dl_args().parse_args() + if not arguments.reset: + print("Your config file is corrupted! Run 'qobuz-dl -r' to fix this\n") + if arguments.reset: sys.exit(reset_config(CONFIG_FILE)) - directory = musicDir(arguments.d) + "/" + directory = musicDir(arguments.directory) + Qz = qopy.Client(email, password, app_id, secrets) - if not arguments.i: - interactive(Qz, directory, arguments.l, not arguments.a) + 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) else: - handle_urls(arguments.i, Qz, directory, arguments.q) + download_lucky_mode( + Qz, + arguments.type, + " ".join(arguments.QUERY), + arguments.number, + directory, + arguments.quality, + arguments.embed_art, + ) if __name__ == "__main__": diff --git a/qobuz_dl/commands.py b/qobuz_dl/commands.py new file mode 100644 index 0000000..73fca8a --- /dev/null +++ b/qobuz_dl/commands.py @@ -0,0 +1,105 @@ +import argparse + + +def qobuz_dl_args( + default_quality=6, default_limit=10, default_folder="Qobuz Downloads" +): + parser = argparse.ArgumentParser( + prog="qobuz-dl", + description=( + "The ultimate Qobuz music downloader.\nSee usage" + " examples on https://github.com/vitiko98/qobuz-dl" + ), + formatter_class=argparse.RawTextHelpFormatter, + ) + parser.add_argument( + "-r", "--reset", action="store_true", help="create/reset config file" + ) + + subparsers = parser.add_subparsers( + title="commands", + description="run qobuz-dl --help for more info\n(e.g. qobuz-dl fun --help)", + dest="command", + ) + + def add_common_arg(custom_parser): + custom_parser.add_argument( + "-e", "--embed-art", action="store_true", help="embed cover art into files" + ) + custom_parser.add_argument( + "-d", + "--directory", + metavar="PATH", + default=default_folder, + help='directory for downloads (default: "{}")'.format(default_folder), + ) + custom_parser.add_argument( + "-q", + "--quality", + metavar="int", + default=default_quality, + choices=[5, 6, 7, 27], + help=( + 'audio "quality" (5, 6, 7, 27)\n' + "[320, LOSSLESS, 24B <96KHZ, 24B >96KHZ] (default: 6)" + ), + ) + custom_parser.add_argument( + "-z", "--zip", action="store_true", help="zip the downloaded item(s)" + ) + + interactive = subparsers.add_parser( + "fun", + 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)", + ) + add_common_arg(interactive) + + download = subparsers.add_parser( + "dl", + description="Download by album/track/artist/label/playlist URL.", + help="input mode", + ) + add_common_arg(download) + download.add_argument( + "SOURCE", + metavar="SOURCE", + nargs="+", + help=("one or more URLs (space separated) or a text file"), + ) + + lucky = subparsers.add_parser( + "lucky", + description="Download the first albums returned from a Qobuz search.", + help="lucky mode", + ) + lucky.add_argument( + "-t", + "--type", + default="album", + help="type of items to search (artist, album, track, playlist) (default: album)", + ) + lucky.add_argument( + "-n", + "--number", + metavar="int", + default=default_limit, + help="number of results to download (default: 1)", + ) + add_common_arg(lucky) + lucky.add_argument("QUERY", nargs="+", help="search query") + + return parser diff --git a/qobuz_dl/downloader.py b/qobuz_dl/downloader.py index 52ddc14..8436178 100644 --- a/qobuz_dl/downloader.py +++ b/qobuz_dl/downloader.py @@ -7,7 +7,7 @@ from tqdm import tqdm import qobuz_dl.metadata as metadata -def req_tqdm(url, fname, track_name): +def tqdm_download(url, fname, track_name): r = requests.get(url, allow_redirects=True, stream=True) total = int(r.headers.get("content-length", 0)) with open(fname, "wb") as file, tqdm( @@ -25,42 +25,115 @@ def req_tqdm(url, fname, track_name): def mkDir(dirn): try: - os.mkdir(dirn) + os.makedirs(dirn, exist_ok=True) except FileExistsError: - print("Warning: folder already exists. Overwriting...") + pass -def getDesc(u, mt): - return "{} [{}/{}]".format(mt["title"], u["bit_depth"], u["sampling_rate"]) +def getDesc(u, mt, multiple=None): + return "{} [{}/{}]".format( + ("[Disc {}] {}".format(multiple, mt["title"])) if multiple else mt["title"], + u["bit_depth"], + u["sampling_rate"], + ) -def getBooklet(i, dirn): - req_tqdm(i, dirn + "/booklet.pdf", "Downloading booklet") +def get_format(album_dict, quality): + try: + if int(quality) == 5: + return "MP3" + if album_dict["maximum_bit_depth"] == 16 and int(quality) < 7: + return "FLAC" + except KeyError: + return "Unknown" + return "Hi-Res" -def getCover(i, dirn): - req_tqdm(i, dirn + "/cover.jpg", "Downloading cover art") +def get_extra(i, dirn, extra="cover.jpg"): + tqdm_download(i, os.path.join(dirn, extra), "Downloading " + extra.split(".")[0]) # Download and tag a file -def downloadItem(dirn, count, parse, meta, album, url, is_track, mp3): - fname = ( - "{}/{:02}.mp3".format(dirn, count) - if mp3 - else "{}/{:02}.flac".format(dirn, count) +def download_and_tag( + root_dir, + tmp_count, + track_url_dict, + track_metadata, + album_or_track_metadata, + is_track, + is_mp3, + embed_art=False, + multiple=None, +): + """ + Download and tag a file + + :param str root_dir: Root directory where the track will be stored + :param int tmp_count: Temporal download file number + :param dict track_url_dict: get_track_url dictionary from Qobuz client + :param dict track_metadata: Track item dictionary from Qobuz client + :param dict album_or_track_metadata: Album/track dictionary from Qobuz client + :param bool is_track + :param bool is_mp3 + :param bool embed_art: Embed cover art into file (FLAC-only) + :param multiple: Multiple disc integer + :type multiple: integer or None + """ + extension = ".mp3" if is_mp3 else ".flac" + + try: + url = track_url_dict["url"] + except KeyError: + print("Track not available for download") + return + + if multiple: + root_dir = os.path.join(root_dir, "Disc " + str(multiple)) + mkDir(root_dir) + + filename = os.path.join(root_dir, ".{:02}".format(tmp_count) + extension) + + new_track_title = sanitize_filename(track_metadata["title"]) + track_file = "{:02}. {}{}".format( + track_metadata["track_number"], new_track_title, extension ) - func = metadata.tag_mp3 if mp3 else metadata.tag_flac - desc = getDesc(parse, meta) - req_tqdm(url, fname, desc) - func(fname, dirn, meta, album, is_track) + final_file = os.path.join(root_dir, track_file) + if os.path.isfile(final_file): + print(track_metadata["title"] + " was already downloaded. Skipping...") + return + + desc = getDesc(track_url_dict, track_metadata, multiple) + tqdm_download(url, filename, desc) + tag_function = metadata.tag_mp3 if is_mp3 else metadata.tag_flac + try: + tag_function( + filename, + root_dir, + final_file, + track_metadata, + album_or_track_metadata, + is_track, + embed_art, + ) + except Exception as e: + print("Error tagging the file: " + str(e)) + os.remove(filename) -# Iterate over IDs by type calling downloadItem -def iterateIDs(client, id, path, quality, album=False): +def download_id_by_type(client, item_id, path, quality, album=False, embed_art=False): + """ + Download and get metadata by ID and type (album or track) + + :param Qopy client: qopy Client + :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 + """ count = 0 if album: - meta = client.get_album_meta(id) + meta = client.get_album_meta(item_id) album_title = ( "{} ({})".format(meta["title"], meta["version"]) if meta["version"] @@ -71,35 +144,42 @@ def iterateIDs(client, id, path, quality, album=False): meta["artist"]["name"], album_title, meta["release_date_original"].split("-")[0], + get_format(meta, quality), ) - sanitized_title = sanitize_filename("{} - {} [{}]".format(*dirT)) - dirn = path + sanitized_title + sanitized_title = sanitize_filename("{} - {} [{}] [{}]".format(*dirT)) + dirn = os.path.join(path, sanitized_title) mkDir(dirn) - getCover(meta["image"]["large"], dirn) + get_extra(meta["image"]["large"], dirn) if "goodies" in meta: try: - getBooklet(meta["goodies"][0]["url"], dirn) + get_extra(meta["goodies"][0]["url"], dirn, "booklet.pdf") except Exception as e: print("Error: " + e) + media_numbers = [track["media_number"] for track in meta["tracks"]["items"]] + is_multiple = True if len([*{*media_numbers}]) > 1 else False for i in meta["tracks"]["items"]: parse = client.get_track_url(i["id"], quality) - try: - url = parse["url"] - except KeyError: - print("Track is not available for download") - return - if "sample" not in parse: + if "sample" not in parse and parse["sampling_rate"]: is_mp3 = True if int(quality) == 5 else False - downloadItem(dirn, count, parse, i, meta, url, False, is_mp3) + download_and_tag( + dirn, + count, + parse, + i, + meta, + False, + is_mp3, + embed_art, + i["media_number"] if is_multiple else None, + ) else: print("Demo. Skipping") count = count + 1 else: - parse = client.get_track_url(id, quality) - url = parse["url"] + parse = client.get_track_url(item_id, quality) - if "sample" not in parse: - meta = client.get_track_meta(id) + if "sample" not in parse and parse["sampling_rate"]: + meta = client.get_track_meta(item_id) track_title = ( "{} ({})".format(meta["title"], meta["version"]) if meta["version"] @@ -110,13 +190,14 @@ def iterateIDs(client, id, path, quality, album=False): meta["album"]["artist"]["name"], track_title, meta["album"]["release_date_original"].split("-")[0], + get_format(meta, quality), ) - sanitized_title = sanitize_filename("{} - {} [{}]".format(*dirT)) - dirn = path + sanitized_title + sanitized_title = sanitize_filename("{} - {} [{}] [{}]".format(*dirT)) + dirn = os.path.join(path, sanitized_title) mkDir(dirn) - getCover(meta["album"]["image"]["large"], dirn) + get_extra(meta["album"]["image"]["large"], dirn) is_mp3 = True if int(quality) == 5 else False - downloadItem(dirn, count, parse, meta, meta, url, True, is_mp3) + download_and_tag(dirn, count, parse, meta, meta, True, is_mp3, embed_art) else: print("Demo. Skipping") print("\nCompleted\n") diff --git a/qobuz_dl/metadata.py b/qobuz_dl/metadata.py index 67e358f..7ac3328 100644 --- a/qobuz_dl/metadata.py +++ b/qobuz_dl/metadata.py @@ -1,12 +1,22 @@ import os -from mutagen.flac import FLAC +from mutagen.flac import FLAC, Picture from mutagen.mp3 import EasyMP3 -from pathvalidate import sanitize_filename -def tag_flac(file, path, d, album, istrack=True): - audio = FLAC(file) +def tag_flac(filename, root_dir, final_name, d, album, istrack=True, em_image=False): + """ + Tag a FLAC file + + :param str filename: FLAC file path + :param str root_dir: Root dir used to get the cover art + :param str final_name: Final name of the FLAC file (complete path) + :param dict d: Track dictionary from Qobuz_client + :param dict album: Album dictionary from Qobuz_client + :param bool istrack + :param bool em_image: Embed cover art into file + """ + audio = FLAC(filename) audio["TITLE"] = ( "{} ({})".format(d["title"], d["version"]) if d["version"] else d["title"] @@ -38,16 +48,37 @@ def tag_flac(file, path, d, album, istrack=True): audio["ALBUM"] = album["title"] # ALBUM TITLE audio["YEAR"] = album["release_date_original"].split("-")[0] # YEAR + emb_image = os.path.join(root_dir, "cover.jpg") + if os.path.isfile(emb_image) and em_image: + try: + image = Picture() + image.type = 3 + image.mime = "image/jpeg" + image.desc = "cover" + with open(emb_image, "rb") as img: + image.data = img.read() + audio.add_picture(image) + except Exception as e: + print("Error embedding image: " + str(e)) + audio.save() - title = sanitize_filename(d["title"]) - try: - os.rename(file, "{}/{:02}. {}.flac".format(path, d["track_number"], title)) - except FileExistsError: - print("File already exists. Skipping...") + os.rename(filename, final_name) -def tag_mp3(file, path, d, album, istrack=True): - audio = EasyMP3(file) +def tag_mp3(filename, root_dir, final_name, d, album, istrack=True, em_image=False): + """ + Tag a mp3 file + + :param str filename: mp3 file path + :param str root_dir: Root dir used to get the cover art + :param str final_name: Final name of the mp3 file (complete path) + :param dict d: Track dictionary from Qobuz_client + :param dict album: Album dictionary from Qobuz_client + :param bool istrack + :param bool em_image: Embed cover art into file + """ + # TODO: add embedded cover art support for mp3 + audio = EasyMP3(filename) audio["title"] = ( "{} ({})".format(d["title"], d["version"]) if d["version"] else d["title"] @@ -77,8 +108,4 @@ def tag_mp3(file, path, d, album, istrack=True): audio["date"] = album["release_date_original"].split("-")[0] # YEAR audio.save() - title = sanitize_filename(d["title"]) - try: - os.rename(file, "{}/{:02}. {}.mp3".format(path, d["track_number"], title)) - except FileExistsError: - print("File already exists. Skipping...") + os.rename(filename, final_name) diff --git a/qobuz_dl/qopy.py b/qobuz_dl/qopy.py index 1a3f097..5176874 100644 --- a/qobuz_dl/qopy.py +++ b/qobuz_dl/qopy.py @@ -115,7 +115,7 @@ class Client: self.uat = usr_info["user_auth_token"] self.session.headers.update({"X-User-Auth-Token": self.uat}) self.label = usr_info["user"]["credential"]["parameters"]["short_label"] - print("Membership: {}".format(self.label)) + print("Membership: {}\n".format(self.label)) def multi_meta(self, epoint, key, id, type): total = 1 @@ -154,6 +154,12 @@ class Client: def search_albums(self, query, limit): return self.api_call("album/search", query=query, limit=limit) + def search_artists(self, query, limit): + return self.api_call("artist/search", query=query, limit=limit) + + def search_playlists(self, query, limit): + return self.api_call("playlist/search", query=query, limit=limit) + def search_tracks(self, query, limit): return self.api_call("track/search", query=query, limit=limit) diff --git a/qobuz_dl/search.py b/qobuz_dl/search.py index aab0e04..8b91363 100644 --- a/qobuz_dl/search.py +++ b/qobuz_dl/search.py @@ -8,6 +8,7 @@ class Search: 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)) diff --git a/setup.py b/setup.py index b4fe120..5d606f1 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ if os.name == "nt": setup( name=pkg_name, - version="0.4.2", + version="0.5", author="Vitiko", author_email="vhnz98@gmail.com", description="The complete Lossless and Hi-Res music downloader for Qobuz", @@ -38,5 +38,6 @@ setup( python_requires=">=3.6", ) -# python setup.py sdist +# python3 setup.py sdist bdist_wheel +# rm -f dist/* # twine upload dist/*