diff --git a/.gitignore b/.gitignore index d84fce7..3552c94 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,147 @@ Qobuz Downloads *__pycache* -.env \ No newline at end of file +.env +__pycache__/ +*.py[cod] +*$py.class +subtitle_search.py + +# C extensions +*.so + +*.json +*.txt +*.db + +*.txt + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ diff --git a/README.md b/README.md index 0e3d171..f93aa24 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Seach and download Lossless and Hi-Res music from [Qobuz](https://www.qobuz.com/ * Download FLAC and MP3 files from Qobuz * Search and download music directly from your terminal with interactive mode * Queue support -* URL input mode with download support for albums, tracks, artists, playlists and labels +* Input url mode with download support for albums, tracks, artists, playlists and labels ## Getting started @@ -17,45 +17,30 @@ Seach and download Lossless and Hi-Res music from [Qobuz](https://www.qobuz.com/ > Note 2: You'll need an **active subscription** -#### Install requirements with pip +#### Install qobuz-dl with pip +##### Linux / MAC OS / Windows +``` +pip3 install qobuz-dl --user +``` +#### Run qobuz-dl and enter your credentials ##### Linux / MAC OS ``` -pip3 install -r requirements.txt --user +qobuz-dl ``` -##### Windows 10 +or ``` -pip3 install windows-curses -pip3 install -r requirements.txt -``` -#### Add your credentials to `config.py` file -```python -email = "your@email.com" -password = "your_password" +qdl ``` -In addition to your credentials, you can also use the `config.py` file to -change other default values: -```python -default_folder = "Qobuz Downloads" -default_limit = 10 -default_quality = 6 -``` +> If something fails, run `qobuz-dl -r` to reset your config file. -#### Run qobuz-dl -##### Linux / MAC OS -``` -python3 main.py -``` -##### Windows 10 -``` -python.exe main.py -``` ## Usage ``` -usage: python3 main.py [-h] [-a] [-i] [-q int] [-l int] [-d PATH] +usage: qobuz-dl [-h] [-a] [-r] [-i Album/track URL] [-q int] [-l int] [-d PATH] 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] diff --git a/config.py b/config.py deleted file mode 100644 index cdf3e2e..0000000 --- a/config.py +++ /dev/null @@ -1,15 +0,0 @@ -import os - -# Qobuz credentials (Don't remove the quotes!) -email = "your@email.com" -password = "your_password" - -# Default folder where the releases are downloaded -default_folder = "Qobuz Downloads" - -# Default per type results limit -default_limit = 10 - -# Default quality for url input mode. This will be ignored in interactive mode -# (5, 6, 7, 27) [320, LOSSLESS, 24B <96KHZ, 24B >96KHZ] -default_quality = 6 diff --git a/main.py b/main.py index eeb0f4b..39cf467 100644 --- a/main.py +++ b/main.py @@ -1,18 +1,51 @@ import argparse +import configparser import os import re import sys from pick import pick -import config +import qo_utils.spoofbuz as spoofbuz from qo_utils import downloader, qopy from qo_utils.search import Search +OS_CONFIG = os.path.join(os.environ["HOME"], ".config") or os.environ.get("APPDATA") +CONFIG_PATH = os.path.join(OS_CONFIG, "qobuz-dl") +CONFIG_FILE = os.path.join(CONFIG_PATH, "config.ini") -def getArgs(): + +def reset_config(config_file): + print("Creating config file: " + config_file) + config = configparser.ConfigParser() + config["DEFAULT"]["email"] = input("\nEnter your email:\n- ") + config["DEFAULT"]["password"] = input("\nEnter your password\n- ") + config["DEFAULT"]["default_folder"] = ( + input("\nFolder for downloads (leave empy for default 'Qobuz Downloads')\n- ") + or "Qobuz Downloads" + ) + config["DEFAULT"]["default_quality"] = ( + input( + "\nDownload quality (5, 6, 7, 27) " + "[320, LOSSLESS, 24B <96KHZ, 24B >96KHZ]" + "\n(leave empy for default '6')\n- " + ) + or "6" + ) + config["DEFAULT"]["default_limit"] = "10" + print("Getting tokens. Please wait...") + spoofer = spoofbuz.Spoofer() + config["DEFAULT"]["app_id"] = str(spoofer.getAppId()) + config["DEFAULT"]["secrets"] = ",".join(spoofer.getSecrets().values()) + with open(config_file, "w") as configfile: + config.write(configfile) + print("Config file updated.") + + +def getArgs(default_quality=6, default_limit=10, default_folder="Qobuz Downloads"): parser = argparse.ArgumentParser(prog="python3 main.py") 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 URL", @@ -21,22 +54,20 @@ def getArgs(): parser.add_argument( "-q", metavar="int", - default=config.default_quality, + default=default_quality, help="quality for url input mode (5, 6, 7, 27) (default: 6)", ) parser.add_argument( "-l", metavar="int", - default=config.default_limit, + default=default_limit, help="limit of search results by type (default: 10)", ) parser.add_argument( "-d", metavar="PATH", - default=config.default_folder, - help="custom directory for downloads (default: '{}')".format( - config.default_folder - ), + default=default_folder, + help="custom directory for downloads (default: '{}')".format(default_folder), ) return parser.parse_args() @@ -151,9 +182,42 @@ def interactive(Qz, path, limit, tracks=True): def main(): - arguments = getArgs() + if not os.path.isdir(CONFIG_PATH) or not os.path.isfile(CONFIG_FILE): + try: + os.mkdir(CONFIG_PATH) + except FileExistsError: + pass + reset_config(CONFIG_FILE) + + email = None + password = None + app_id = None + secrets = None + + config = configparser.ConfigParser() + config.read(CONFIG_FILE) + + try: + email = config["DEFAULT"]["email"] + password = config["DEFAULT"]["password"] + default_folder = config["DEFAULT"]["default_folder"] + default_limit = config["DEFAULT"]["default_limit"] + default_quality = config["DEFAULT"]["default_quality"] + app_id = config["DEFAULT"]["app_id"] + secrets = [ + secret for secret in config["DEFAULT"]["secrets"].split(",") if secret + ] + arguments = getArgs(default_quality, default_limit, default_folder) + except KeyError: + print("Your config file is corrupted! Run 'qobuz-dl -r' to fix this\n") + arguments = getArgs() + + if arguments.r: + sys.exit(reset_config(CONFIG_FILE)) + directory = musicDir(arguments.d) + "/" - Qz = qopy.Client(config.email, config.password) + Qz = qopy.Client(email, password, app_id, secrets) + if not arguments.i: interactive(Qz, directory, arguments.l, not arguments.a) else: diff --git a/qo_utils/metadata.py b/qo_utils/metadata.py deleted file mode 100644 index bffa6e8..0000000 --- a/qo_utils/metadata.py +++ /dev/null @@ -1,150 +0,0 @@ -import os - -from mutagen.flac import FLAC -from mutagen.mp3 import EasyMP3 -from pathvalidate import sanitize_filename - - -def tag_flac(file, path, d, album, istrack=True): - audio = FLAC(file) - try: - d["version"] - except KeyError: - audio["TITLE"] = d["title"] - dversion_exist = 0 - else: - if d["version"] is None: - audio["TITLE"] = d["title"] # TRACK TITLE - dversion_exist = 0 - else: - audio["TITLE"] = d["title"] + " " + "(" + d["version"] + ")" - dversion_exist = 1 - # if d["version"] is None: - # audio["TITLE"] = d["title"]# TRACK TITLE - # else: - # audio["TITLE"] = d["title"] + ' ' + '(' + d["version"] + ')' - - audio["TRACKNUMBER"] = str(d["track_number"]) # TRACK NUMBER - try: - audio["COMPOSER"] = d["composer"]["name"] # COMPOSER - except KeyError: - pass - - try: - audio["ARTIST"] = d["performer"]["name"] # TRACK ARTIST - except KeyError: - if istrack: - audio["ARTIST"] = d["album"]["artist"]["name"] # TRACK ARTIST - else: - audio["ARTIST"] = album["artist"]["name"] - - if istrack: - if dversion_exist == 0: - audio["GENRE"] = ", ".join(d["album"]["genres_list"]) # GENRE - audio["ALBUMARTIST"] = d["album"]["artist"]["name"] # ALBUM ARTIST - audio["TRACKTOTAL"] = str(d["album"]["tracks_count"]) # TRACK TOTAL - audio["ALBUM"] = d["album"]["title"] # ALBUM TITLE - audio["YEAR"] = d["album"]["release_date_original"].split("-")[0] - else: - audio["GENRE"] = ", ".join(d["album"]["genres_list"]) # GENRE - audio["ALBUMARTIST"] = d["album"]["artist"]["name"] # ALBUM ARTIST - audio["TRACKTOTAL"] = str(d["album"]["tracks_count"]) # TRACK TOTAL - audio["ALBUM"] = ( - d["album"]["title"] + " " + "(" + d["album"]["version"] + ")" - ) # ALBUM TITLE - audio["YEAR"] = d["album"]["release_date_original"].split("-")[0] - else: - if dversion_exist == 0: - audio["GENRE"] = ", ".join(album["genres_list"]) # GENRE - audio["ALBUMARTIST"] = album["artist"]["name"] # ALBUM ARTIST - audio["TRACKTOTAL"] = str(album["tracks_count"]) # TRACK TOTAL - audio["ALBUM"] = album["title"] # ALBUM TITLE - audio["YEAR"] = album["release_date_original"].split("-")[0] # YEAR - else: - audio["GENRE"] = ", ".join(album["genres_list"]) # GENRE - audio["ALBUMARTIST"] = album["artist"]["name"] # ALBUM ARTIST - audio["TRACKTOTAL"] = str(album["tracks_count"]) # TRACK TOTAL - audio["ALBUM"] = ( - album["title"] + " " + "(" + album["version"] + ")" - ) # ALBUM TITLE - audio["YEAR"] = album["release_date_original"].split("-")[0] # YEAR - - audio.save() - if dversion_exist == 1: - title = sanitize_filename(d["title"] + " " + "(" + d["version"] + ")") - else: - title = sanitize_filename(d["title"]) - try: - os.rename(file, "{}/{:02}. {}.flac".format(path, d["track_number"], title)) - except FileExistsError: - print("File already exists. Skipping...") - - -def tag_mp3(file, path, d, album, istrack=True): # needs to be fixed - audio = EasyMP3(file) - try: - d["version"] - except KeyError: - audio["TITLE"] = d["title"] - dversion_exist = 0 - else: - if d["version"] is None: - audio["TITLE"] = d["title"] # TRACK TITLE - dversion_exist = 0 - else: - audio["TITLE"] = d["title"] + " " + "(" + d["version"] + ")" - dversion_exist = 1 - - audio["tracknumber"] = str(d["track_number"]) - try: - audio["composer"] = d["composer"]["name"] - except KeyError: - pass - try: - audio["artist"] = d["performer"]["name"] # TRACK ARTIST - except KeyError: - if istrack: - audio["artist"] = d["album"]["artist"]["name"] # TRACK ARTIST - else: - audio["artist"] = album["artist"]["name"] - if istrack: - if dversion_exist == 1: - audio["genre"] = ", ".join(d["album"]["genres_list"]) # GENRE - audio["albumartist"] = d["album"]["artist"]["name"] # ALBUM ARTIST - audio["album"] = ( - d["album"]["title"] + " " + "(" + d["album"]["version"] + ")" - ) # ALBUM TITLE - audio["date"] = d["album"]["release_date_original"].split("-")[0] - else: - audio["genre"] = ", ".join(d["album"]["genres_list"]) # GENRE - audio["albumartist"] = d["album"]["artist"]["name"] # ALBUM ARTIST - audio["album"] = d["album"]["title"] # ALBUM TITLE - audio["date"] = d["album"]["release_date_original"].split("-")[0] - else: - if album["version"] is not None: - audio["GENRE"] = ", ".join(album["genres_list"]) # GENRE - audio["albumartist"] = album["artist"]["name"] # ALBUM ARTIST - try: - album["version"] - except KeyError: - audio["album"] = album["title"] - else: - audio["album"] = ( - album["title"] + " " + "(" + album["version"] + ")" - ) # ALBUM TITLE - audio["date"] = album["release_date_original"].split("-")[0] # YEAR - else: - 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 - - audio.save() - if dversion_exist == 1: - title = sanitize_filename(d["title"] + " " + "(" + d["version"] + ")") - else: - title = sanitize_filename(d["title"]) - try: - os.rename(file, "{}/{:02}. {}.mp3".format(path, d["track_number"], title)) - except FileExistsError: - print("File already exists. Skipping...") diff --git a/qo_utils/__init__.py b/qobuz_dl/__init__.py similarity index 53% rename from qo_utils/__init__.py rename to qobuz_dl/__init__.py index bab2a20..b08c1c5 100644 --- a/qo_utils/__init__.py +++ b/qobuz_dl/__init__.py @@ -1 +1,2 @@ from .qopy import Client +from .cli import main diff --git a/qobuz_dl/cli.py b/qobuz_dl/cli.py new file mode 100644 index 0000000..8cf8a3c --- /dev/null +++ b/qobuz_dl/cli.py @@ -0,0 +1,228 @@ +import argparse +import configparser +import os +import re +import sys + +from pick import pick + +import qobuz_dl.spoofbuz as spoofbuz +from qobuz_dl import downloader, qopy +from qobuz_dl.search import Search + +OS_CONFIG = os.path.join(os.environ["HOME"], ".config") or os.environ.get("APPDATA") +CONFIG_PATH = os.path.join(OS_CONFIG, "qobuz-dl") +CONFIG_FILE = os.path.join(CONFIG_PATH, "config.ini") + + +def reset_config(config_file): + print("Creating config file: " + config_file) + config = configparser.ConfigParser() + config["DEFAULT"]["email"] = input("\nEnter your email:\n- ") + config["DEFAULT"]["password"] = input("\nEnter your password\n- ") + config["DEFAULT"]["default_folder"] = ( + input("\nFolder for downloads (leave empy for default 'Qobuz Downloads')\n- ") + or "Qobuz Downloads" + ) + config["DEFAULT"]["default_quality"] = ( + input( + "\nDownload quality (5, 6, 7, 27) " + "[320, LOSSLESS, 24B <96KHZ, 24B >96KHZ]" + "\n(leave empy for default '6')\n- " + ) + or "6" + ) + config["DEFAULT"]["default_limit"] = "10" + print("Getting tokens. Please wait...") + spoofer = spoofbuz.Spoofer() + config["DEFAULT"]["app_id"] = str(spoofer.getAppId()) + config["DEFAULT"]["secrets"] = ",".join(spoofer.getSecrets().values()) + with open(config_file, "w") as configfile: + config.write(configfile) + 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 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) + if not os.path.isdir(fix): + os.mkdir(fix) + 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): + q = ["5", "6", "7", "27"] + quality = q[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 + ) + + +def fromUrl(Qz, id, path, quality, album=True): + downloader.iterateIDs(Qz, id, path, str(quality), album) + + +def handle_urls(url, client, path, quality): + 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) + print("Downloading {}...".format(url_type)) + except KeyError: + print("Invalid url. Use urls from https://play.qobuz.com!") + return + if type_dict["func"]: + items = [ + item[type_dict["iterable_key"]]["items"] + for item in type_dict["func"](item_id) + ][0] + for item in items: + fromUrl( + client, + item["id"], + path, + quality, + True if type_dict["iterable_key"] == "albums" else False, + ) + else: + fromUrl(client, item_id, path, quality, type_dict["album"]) + + +def interactive(Qz, path, limit, tracks=True): + 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) + except KeyboardInterrupt: + sys.exit("\nBye") + + +def main(): + if not os.path.isdir(CONFIG_PATH) or not os.path.isfile(CONFIG_FILE): + try: + os.mkdir(CONFIG_PATH) + except FileExistsError: + pass + reset_config(CONFIG_FILE) + + email = None + password = None + app_id = None + secrets = None + + config = configparser.ConfigParser() + config.read(CONFIG_FILE) + + try: + email = config["DEFAULT"]["email"] + password = config["DEFAULT"]["password"] + default_folder = config["DEFAULT"]["default_folder"] + default_limit = config["DEFAULT"]["default_limit"] + default_quality = config["DEFAULT"]["default_quality"] + app_id = config["DEFAULT"]["app_id"] + secrets = [ + secret for secret in config["DEFAULT"]["secrets"].split(",") if secret + ] + arguments = getArgs(default_quality, default_limit, default_folder) + except KeyError: + print("Your config file is corrupted! Run 'qobuz-dl -r' to fix this\n") + arguments = getArgs() + + if arguments.r: + sys.exit(reset_config(CONFIG_FILE)) + + directory = musicDir(arguments.d) + "/" + Qz = qopy.Client(email, password, app_id, secrets) + + if not arguments.i: + interactive(Qz, directory, arguments.l, not arguments.a) + else: + handle_urls(arguments.i, Qz, directory, arguments.q) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/qo_utils/downloader.py b/qobuz_dl/downloader.py similarity index 74% rename from qo_utils/downloader.py rename to qobuz_dl/downloader.py index 7c73106..52ddc14 100644 --- a/qo_utils/downloader.py +++ b/qobuz_dl/downloader.py @@ -4,7 +4,7 @@ import requests from pathvalidate import sanitize_filename from tqdm import tqdm -from qo_utils import metadata +import qobuz_dl.metadata as metadata def req_tqdm(url, fname, track_name): @@ -31,12 +31,7 @@ def mkDir(dirn): def getDesc(u, mt): - return "{}{} [{}/{}]".format( - mt["title"], - (" (" + mt["version"] + ")") if mt["version"] else "", - u["bit_depth"], - u["sampling_rate"], - ) + return "{} [{}/{}]".format(mt["title"], u["bit_depth"], u["sampling_rate"]) def getBooklet(i, dirn): @@ -66,25 +61,26 @@ def iterateIDs(client, id, path, quality, album=False): if album: meta = client.get_album_meta(id) - - print( - "\nDownloading: {0} {1}\n".format( - meta["title"], - ("(" + meta["version"] + ")") if meta["version"] else " ", - ) + album_title = ( + "{} ({})".format(meta["title"], meta["version"]) + if meta["version"] + else meta["title"] ) + print("\nDownloading: {}\n".format(album_title)) dirT = ( meta["artist"]["name"], - meta["title"], - (" " + meta["version"]) if meta["version"] else "", + album_title, meta["release_date_original"].split("-")[0], ) - sanitized_title = sanitize_filename("{} - {}{} [{}]".format(*dirT)) # aa-{} + sanitized_title = sanitize_filename("{} - {} [{}]".format(*dirT)) dirn = path + sanitized_title mkDir(dirn) getCover(meta["image"]["large"], dirn) if "goodies" in meta: - getBooklet(meta["goodies"][0]["url"], dirn) + try: + getBooklet(meta["goodies"][0]["url"], dirn) + except Exception as e: + print("Error: " + e) for i in meta["tracks"]["items"]: parse = client.get_track_url(i["id"], quality) try: @@ -104,19 +100,18 @@ def iterateIDs(client, id, path, quality, album=False): if "sample" not in parse: meta = client.get_track_meta(id) - print( - "\nDownloading: {0} {1}\n".format( - meta["title"], - ("(" + meta["version"] + ")") if meta["version"] else " ", - ) + track_title = ( + "{} ({})".format(meta["title"], meta["version"]) + if meta["version"] + else meta["title"] ) + print("\nDownloading: {}\n".format(track_title)) dirT = ( meta["album"]["artist"]["name"], - meta["album"]["title"], - (" " + meta["album"]["version"]) if meta["album"]["version"] else "", + track_title, meta["album"]["release_date_original"].split("-")[0], ) - sanitized_title = sanitize_filename("{} - {}{} [{}]".format(*dirT)) + sanitized_title = sanitize_filename("{} - {} [{}]".format(*dirT)) dirn = path + sanitized_title mkDir(dirn) getCover(meta["album"]["image"]["large"], dirn) diff --git a/qo_utils/exceptions.py b/qobuz_dl/exceptions.py similarity index 100% rename from qo_utils/exceptions.py rename to qobuz_dl/exceptions.py diff --git a/qobuz_dl/metadata.py b/qobuz_dl/metadata.py new file mode 100644 index 0000000..67e358f --- /dev/null +++ b/qobuz_dl/metadata.py @@ -0,0 +1,84 @@ +import os + +from mutagen.flac import FLAC +from mutagen.mp3 import EasyMP3 +from pathvalidate import sanitize_filename + + +def tag_flac(file, path, d, album, istrack=True): + audio = FLAC(file) + + audio["TITLE"] = ( + "{} ({})".format(d["title"], d["version"]) if d["version"] else d["title"] + ) # TRACK TITLE + audio["TRACKNUMBER"] = str(d["track_number"]) # TRACK NUMBER + try: + audio["COMPOSER"] = d["composer"]["name"] # COMPOSER + except KeyError: + pass + + try: + audio["ARTIST"] = d["performer"]["name"] # TRACK ARTIST + except KeyError: + if istrack: + audio["ARTIST"] = d["album"]["artist"]["name"] # TRACK ARTIST + else: + audio["ARTIST"] = album["artist"]["name"] + + if istrack: + audio["GENRE"] = ", ".join(d["album"]["genres_list"]) # GENRE + audio["ALBUMARTIST"] = d["album"]["artist"]["name"] # ALBUM ARTIST + audio["TRACKTOTAL"] = str(d["album"]["tracks_count"]) # TRACK TOTAL + audio["ALBUM"] = d["album"]["title"] # ALBUM TITLE + audio["YEAR"] = d["album"]["release_date_original"].split("-")[0] + else: + audio["GENRE"] = ", ".join(album["genres_list"]) # GENRE + audio["ALBUMARTIST"] = album["artist"]["name"] # ALBUM ARTIST + audio["TRACKTOTAL"] = str(album["tracks_count"]) # TRACK TOTAL + audio["ALBUM"] = album["title"] # ALBUM TITLE + audio["YEAR"] = album["release_date_original"].split("-")[0] # YEAR + + 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...") + + +def tag_mp3(file, path, d, album, istrack=True): + audio = EasyMP3(file) + + audio["title"] = ( + "{} ({})".format(d["title"], d["version"]) if d["version"] else d["title"] + ) # TRACK TITLE + audio["tracknumber"] = str(d["track_number"]) + try: + audio["composer"] = d["composer"]["name"] + except KeyError: + pass + try: + audio["artist"] = d["performer"]["name"] # TRACK ARTIST + except KeyError: + if istrack: + audio["artist"] = d["album"]["artist"]["name"] # TRACK ARTIST + else: + audio["artist"] = album["artist"]["name"] + + if istrack: + audio["genre"] = ", ".join(d["album"]["genres_list"]) # GENRE + audio["albumartist"] = d["album"]["artist"]["name"] # ALBUM ARTIST + 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["albumartist"] = album["artist"]["name"] # ALBUM ARTIST + audio["album"] = album["title"] # ALBUM TITLE + 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...") diff --git a/qo_utils/qopy.py b/qobuz_dl/qopy.py similarity index 91% rename from qo_utils/qopy.py rename to qobuz_dl/qopy.py index 4a26d54..1a3f097 100644 --- a/qo_utils/qopy.py +++ b/qobuz_dl/qopy.py @@ -7,20 +7,21 @@ import time import requests -from qo_utils import spoofbuz -from qo_utils.exceptions import ( +from qobuz_dl.exceptions import ( AuthenticationError, IneligibleError, InvalidAppIdError, InvalidAppSecretError, ) +RESET = "Reset your credentials with 'qobuz-dl -r'" + class Client: - def __init__(self, email, pwd): - print("Getting tokens...") - self.spoofer = spoofbuz.Spoofer() - self.id = self.spoofer.getAppId() + def __init__(self, email, pwd, app_id, secrets): + print("Logging...") + self.secrets = secrets + self.id = app_id self.session = requests.Session() self.session.headers.update( { @@ -96,14 +97,14 @@ class Client: # Do ref header. if epoint == "user/login": if r.status_code == 401: - raise AuthenticationError("Invalid credentials.") + raise AuthenticationError("Invalid credentials.\n" + RESET) elif r.status_code == 400: - raise InvalidAppIdError("Invalid app id.") + raise InvalidAppIdError("Invalid app id.\n" + RESET) else: print("Logged: OK") elif epoint in ["track/getFileUrl", "userLibrary/getAlbumsList"]: if r.status_code == 400: - raise InvalidAppSecretError("Invalid app secret.") + raise InvalidAppSecretError("Invalid app secret.\n" + RESET) r.raise_for_status() return r.json() @@ -182,7 +183,7 @@ class Client: return False def cfg_setup(self): - for secret in self.spoofer.getSecrets().values(): + for secret in self.secrets: if self.test_secret(secret): self.sec = secret break diff --git a/qo_utils/search.py b/qobuz_dl/search.py similarity index 58% rename from qo_utils/search.py rename to qobuz_dl/search.py index 5c4e375..aab0e04 100644 --- a/qo_utils/search.py +++ b/qobuz_dl/search.py @@ -28,23 +28,14 @@ class Search: self.Total.append("[RELEASE] {} - {} - {} [{}]".format(*items)) self.appendInfo(i, True) except KeyError: - try: - items = ( - i["performer"]["name"], - i["title"], - self.seconds(i["duration"]), - "HI-RES" if i["hires"] else "Lossless", - ) - self.Total.append("[TRACK] {} - {} - {} [{}]".format(*items)) - self.appendInfo(i, False) - except KeyError: - items = ( - i["title"], - self.seconds(i["duration"]), - "HI-RES" if i["hires"] else "Lossless", - ) - self.Total.append("[TRACK] {} [{}]".format(*items)) - self.appendInfo(i, False) + items = ( + i["performer"]["name"], + 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) diff --git a/qo_utils/spoofbuz.py b/qobuz_dl/spoofbuz.py similarity index 100% rename from qo_utils/spoofbuz.py rename to qobuz_dl/spoofbuz.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c6d4d65 --- /dev/null +++ b/setup.py @@ -0,0 +1,42 @@ +from setuptools import setup, find_packages +import os + +pkg_name = "qobuz-dl" + + +def read_file(fname): + with open(fname, "r") as f: + return f.read() + + +requirements = read_file("requirements.txt").strip().split() +if os.name == "nt": + requirements.append("windows-curses") + +setup( + name=pkg_name, + version="0.4", + author="Vitiko", + author_email="vhnz98@gmail.com", + description="The complete Lossless and Hi-Res music downloader for Qobuz", + long_description=read_file("README.md"), + long_description_content_type="text/markdown", + url="https://github.com/vitiko98/Qobuz-DL", + install_requires=requirements, + entry_points={ + "console_scripts": [ + "qobuz-dl = qobuz_dl:main", + "qdl = qobuz_dl:main", + ], + }, + packages=find_packages(), + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU General Public License (GPL)", + "Operating System :: OS Independent", + ], + python_requires=">=3.6", +) + +# python setup.py sdist +# twine upload dist/*