pip package; reverse pull requests that broke qobuz-dl (my bad for not

testing); add remix naming support
This commit is contained in:
vitiko98 2020-12-07 15:58:51 -04:00
parent 2f63df8c0b
commit 25180526be
14 changed files with 626 additions and 256 deletions

144
.gitignore vendored
View File

@ -1,3 +1,147 @@
Qobuz Downloads Qobuz Downloads
*__pycache* *__pycache*
.env .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/

View File

@ -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 * Download FLAC and MP3 files from Qobuz
* Search and download music directly from your terminal with interactive mode * Search and download music directly from your terminal with interactive mode
* Queue support * 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 ## 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** > 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 ##### Linux / MAC OS
``` ```
pip3 install -r requirements.txt --user qobuz-dl
``` ```
##### Windows 10 or
``` ```
pip3 install windows-curses qdl
pip3 install -r requirements.txt
```
#### Add your credentials to `config.py` file
```python
email = "your@email.com"
password = "your_password"
``` ```
In addition to your credentials, you can also use the `config.py` file to > If something fails, run `qobuz-dl -r` to reset your config file.
change other default values:
```python
default_folder = "Qobuz Downloads"
default_limit = 10
default_quality = 6
```
#### Run qobuz-dl
##### Linux / MAC OS
```
python3 main.py
```
##### Windows 10
```
python.exe main.py
```
## Usage ## 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: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-r create/reset config file
-a enable albums-only search -a enable albums-only search
-i album/track/artist/label/playlist URL run qobuz-dl on URL input mode (download by url) -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] -q int quality (5, 6, 7, 27) (default: 6) [320, LOSSLESS, 24B <96KHZ, 24B >96KHZ]

View File

@ -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

82
main.py
View File

@ -1,18 +1,51 @@
import argparse import argparse
import configparser
import os import os
import re import re
import sys import sys
from pick import pick from pick import pick
import config import qo_utils.spoofbuz as spoofbuz
from qo_utils import downloader, qopy from qo_utils import downloader, qopy
from qo_utils.search import Search 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 = argparse.ArgumentParser(prog="python3 main.py")
parser.add_argument("-a", action="store_true", help="enable albums-only search") 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( parser.add_argument(
"-i", "-i",
metavar="Album/track URL", metavar="Album/track URL",
@ -21,22 +54,20 @@ def getArgs():
parser.add_argument( parser.add_argument(
"-q", "-q",
metavar="int", metavar="int",
default=config.default_quality, default=default_quality,
help="quality for url input mode (5, 6, 7, 27) (default: 6)", help="quality for url input mode (5, 6, 7, 27) (default: 6)",
) )
parser.add_argument( parser.add_argument(
"-l", "-l",
metavar="int", metavar="int",
default=config.default_limit, default=default_limit,
help="limit of search results by type (default: 10)", help="limit of search results by type (default: 10)",
) )
parser.add_argument( parser.add_argument(
"-d", "-d",
metavar="PATH", metavar="PATH",
default=config.default_folder, default=default_folder,
help="custom directory for downloads (default: '{}')".format( help="custom directory for downloads (default: '{}')".format(default_folder),
config.default_folder
),
) )
return parser.parse_args() return parser.parse_args()
@ -151,9 +182,42 @@ def interactive(Qz, path, limit, tracks=True):
def main(): 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() arguments = getArgs()
if arguments.r:
sys.exit(reset_config(CONFIG_FILE))
directory = musicDir(arguments.d) + "/" directory = musicDir(arguments.d) + "/"
Qz = qopy.Client(config.email, config.password) Qz = qopy.Client(email, password, app_id, secrets)
if not arguments.i: if not arguments.i:
interactive(Qz, directory, arguments.l, not arguments.a) interactive(Qz, directory, arguments.l, not arguments.a)
else: else:

View File

@ -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...")

View File

@ -1 +1,2 @@
from .qopy import Client from .qopy import Client
from .cli import main

228
qobuz_dl/cli.py Normal file
View File

@ -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())

View File

@ -4,7 +4,7 @@ import requests
from pathvalidate import sanitize_filename from pathvalidate import sanitize_filename
from tqdm import tqdm from tqdm import tqdm
from qo_utils import metadata import qobuz_dl.metadata as metadata
def req_tqdm(url, fname, track_name): def req_tqdm(url, fname, track_name):
@ -31,12 +31,7 @@ def mkDir(dirn):
def getDesc(u, mt): def getDesc(u, mt):
return "{}{} [{}/{}]".format( return "{} [{}/{}]".format(mt["title"], u["bit_depth"], u["sampling_rate"])
mt["title"],
(" (" + mt["version"] + ")") if mt["version"] else "",
u["bit_depth"],
u["sampling_rate"],
)
def getBooklet(i, dirn): def getBooklet(i, dirn):
@ -66,25 +61,26 @@ def iterateIDs(client, id, path, quality, album=False):
if album: if album:
meta = client.get_album_meta(id) meta = client.get_album_meta(id)
album_title = (
print( "{} ({})".format(meta["title"], meta["version"])
"\nDownloading: {0} {1}\n".format( if meta["version"]
meta["title"], else meta["title"]
("(" + meta["version"] + ")") if meta["version"] else " ",
)
) )
print("\nDownloading: {}\n".format(album_title))
dirT = ( dirT = (
meta["artist"]["name"], meta["artist"]["name"],
meta["title"], album_title,
(" " + meta["version"]) if meta["version"] else "",
meta["release_date_original"].split("-")[0], meta["release_date_original"].split("-")[0],
) )
sanitized_title = sanitize_filename("{} - {}{} [{}]".format(*dirT)) # aa-{} sanitized_title = sanitize_filename("{} - {} [{}]".format(*dirT))
dirn = path + sanitized_title dirn = path + sanitized_title
mkDir(dirn) mkDir(dirn)
getCover(meta["image"]["large"], dirn) getCover(meta["image"]["large"], dirn)
if "goodies" in meta: if "goodies" in meta:
try:
getBooklet(meta["goodies"][0]["url"], dirn) getBooklet(meta["goodies"][0]["url"], dirn)
except Exception as e:
print("Error: " + e)
for i in meta["tracks"]["items"]: for i in meta["tracks"]["items"]:
parse = client.get_track_url(i["id"], quality) parse = client.get_track_url(i["id"], quality)
try: try:
@ -104,19 +100,18 @@ def iterateIDs(client, id, path, quality, album=False):
if "sample" not in parse: if "sample" not in parse:
meta = client.get_track_meta(id) meta = client.get_track_meta(id)
print( track_title = (
"\nDownloading: {0} {1}\n".format( "{} ({})".format(meta["title"], meta["version"])
meta["title"], if meta["version"]
("(" + meta["version"] + ")") if meta["version"] else " ", else meta["title"]
)
) )
print("\nDownloading: {}\n".format(track_title))
dirT = ( dirT = (
meta["album"]["artist"]["name"], meta["album"]["artist"]["name"],
meta["album"]["title"], track_title,
(" " + meta["album"]["version"]) if meta["album"]["version"] else "",
meta["album"]["release_date_original"].split("-")[0], meta["album"]["release_date_original"].split("-")[0],
) )
sanitized_title = sanitize_filename("{} - {}{} [{}]".format(*dirT)) sanitized_title = sanitize_filename("{} - {} [{}]".format(*dirT))
dirn = path + sanitized_title dirn = path + sanitized_title
mkDir(dirn) mkDir(dirn)
getCover(meta["album"]["image"]["large"], dirn) getCover(meta["album"]["image"]["large"], dirn)

84
qobuz_dl/metadata.py Normal file
View File

@ -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...")

View File

@ -7,20 +7,21 @@ import time
import requests import requests
from qo_utils import spoofbuz from qobuz_dl.exceptions import (
from qo_utils.exceptions import (
AuthenticationError, AuthenticationError,
IneligibleError, IneligibleError,
InvalidAppIdError, InvalidAppIdError,
InvalidAppSecretError, InvalidAppSecretError,
) )
RESET = "Reset your credentials with 'qobuz-dl -r'"
class Client: class Client:
def __init__(self, email, pwd): def __init__(self, email, pwd, app_id, secrets):
print("Getting tokens...") print("Logging...")
self.spoofer = spoofbuz.Spoofer() self.secrets = secrets
self.id = self.spoofer.getAppId() self.id = app_id
self.session = requests.Session() self.session = requests.Session()
self.session.headers.update( self.session.headers.update(
{ {
@ -96,14 +97,14 @@ class Client:
# Do ref header. # Do ref header.
if epoint == "user/login": if epoint == "user/login":
if r.status_code == 401: if r.status_code == 401:
raise AuthenticationError("Invalid credentials.") raise AuthenticationError("Invalid credentials.\n" + RESET)
elif r.status_code == 400: elif r.status_code == 400:
raise InvalidAppIdError("Invalid app id.") raise InvalidAppIdError("Invalid app id.\n" + RESET)
else: else:
print("Logged: OK") print("Logged: OK")
elif epoint in ["track/getFileUrl", "userLibrary/getAlbumsList"]: elif epoint in ["track/getFileUrl", "userLibrary/getAlbumsList"]:
if r.status_code == 400: if r.status_code == 400:
raise InvalidAppSecretError("Invalid app secret.") raise InvalidAppSecretError("Invalid app secret.\n" + RESET)
r.raise_for_status() r.raise_for_status()
return r.json() return r.json()
@ -182,7 +183,7 @@ class Client:
return False return False
def cfg_setup(self): def cfg_setup(self):
for secret in self.spoofer.getSecrets().values(): for secret in self.secrets:
if self.test_secret(secret): if self.test_secret(secret):
self.sec = secret self.sec = secret
break break

View File

@ -28,7 +28,6 @@ class Search:
self.Total.append("[RELEASE] {} - {} - {} [{}]".format(*items)) self.Total.append("[RELEASE] {} - {} - {} [{}]".format(*items))
self.appendInfo(i, True) self.appendInfo(i, True)
except KeyError: except KeyError:
try:
items = ( items = (
i["performer"]["name"], i["performer"]["name"],
i["title"], i["title"],
@ -37,14 +36,6 @@ class Search:
) )
self.Total.append("[TRACK] {} - {} - {} [{}]".format(*items)) self.Total.append("[TRACK] {} - {} - {} [{}]".format(*items))
self.appendInfo(i, False) 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)
def getResults(self, tracks=False): def getResults(self, tracks=False):
self.itResults(self.Albums) self.itResults(self.Albums)

42
setup.py Normal file
View File

@ -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/*