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
This commit is contained in:
vitiko98 2020-12-09 14:52:18 -04:00
parent c54eb71713
commit 621c609721
9 changed files with 513 additions and 134 deletions

1
.gitignore vendored
View File

@ -12,6 +12,7 @@ subtitle_search.py
*.json *.json
*.txt *.txt
*.db *.db
*.sh
*.txt *.txt

View File

@ -6,9 +6,12 @@ If you need help or want to report a problem, join [qobuz-dl's discord server](h
## Features ## Features
* 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** or **lucky** mode
* Queue support * Download albums, tracks, artists, playlists and labels with **download** mode
* Input url mode with download support for albums, tracks, artists, playlists and labels * Queue support on **interactive** mode
* Support for albums with multiple discs
* Read URLs from text file
* And more
## Getting started ## Getting started
@ -34,21 +37,93 @@ qobuz-dl.exe
> If something fails, run `qobuz-dl -r` to reset your config file. > 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
``` ```
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: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-r create/reset config file -r, --reset 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) commands:
-q int quality (5, 6, 7, 27) (default: 6) [320, LOSSLESS, 24B <96KHZ, 24B >96KHZ] run qobuz-dl <command> --help for more info
-l int limit of search results by type (default: 10) (e.g. qobuz-dl fun --help)
-d PATH custom directory for downloads (default: 'Qobuz Downloads')
{fun,dl,lucky}
fun interactive mode
dl input mode
lucky lucky mode
``` ```
## A note about Qo-DL ## 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. `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 ## Disclaimer
This tool was written for educational purposes. I will not be responsible if you use this program in bad faith. 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).
Also, you are accepting this: [Qobuz API Terms of Use](https://static.qobuz.com/apps/api/QobuzAPI-TermsofUse.pdf).

View File

@ -5,10 +5,12 @@ import re
import sys import sys
from pick import pick from pick import pick
from pathvalidate import sanitize_filename
import qobuz_dl.spoofbuz as spoofbuz import qobuz_dl.spoofbuz as spoofbuz
from qobuz_dl import downloader, qopy from qobuz_dl import downloader, qopy
from qobuz_dl.search import Search from qobuz_dl.search import Search
from qobuz_dl.commands import qobuz_dl_args
if os.name == "nt": if os.name == "nt":
OS_CONFIG = os.environ.get("APPDATA") OS_CONFIG = os.environ.get("APPDATA")
@ -18,6 +20,8 @@ else:
CONFIG_PATH = os.path.join(OS_CONFIG, "qobuz-dl") CONFIG_PATH = os.path.join(OS_CONFIG, "qobuz-dl")
CONFIG_FILE = os.path.join(CONFIG_PATH, "config.ini") 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): def reset_config(config_file):
print("Creating config file: " + config_file) print("Creating config file: " + config_file)
@ -46,40 +50,11 @@ def reset_config(config_file):
print("Config file updated.") print("Config file updated.")
def getArgs(default_quality=6, default_limit=10, default_folder="Qobuz Downloads"): def musicDir(directory):
parser = argparse.ArgumentParser(prog="qobuz-dl") fix = os.path.normpath(directory)
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)
if not os.path.isdir(fix): if not os.path.isdir(fix):
os.mkdir(fix) print("New directory created: " + fix)
os.makedirs(fix, exist_ok=True)
return fix return fix
@ -91,21 +66,25 @@ def get_id(url):
).group(1) ).group(1)
def processSelected(Qz, path, albums, ids, types, quality): def processSelected(Qz, path, albums, ids, types, quality, embed_art=False):
q = ["5", "6", "7", "27"] quality = [i for i in QUALITIES.keys()][quality[1]]
quality = q[quality[1]]
for alb, id_, type_ in zip(albums, ids, types): for alb, id_, type_ in zip(albums, ids, types):
for al in alb: for al in alb:
downloader.iterateIDs( downloader.download_id_by_type(
Qz, id_[al[1]], path, quality, True if type_[al[1]] else False Qz,
id_[al[1]],
path,
quality,
True if type_[al[1]] else False,
embed_art,
) )
def fromUrl(Qz, id, path, quality, album=True): def fromUrl(Qz, id, path, quality, album=True, embed_art=False):
downloader.iterateIDs(Qz, id, path, str(quality), album) 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 = { possibles = {
"playlist": {"func": client.get_plist_meta, "iterable_key": "tracks"}, "playlist": {"func": client.get_plist_meta, "iterable_key": "tracks"},
"artist": {"func": client.get_artist_meta, "iterable_key": "albums"}, "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] url_type = url.split("/")[3]
type_dict = possibles[url_type] type_dict = possibles[url_type]
item_id = get_id(url) item_id = get_id(url)
print("Downloading {}...".format(url_type)) except (KeyError, IndexError):
except KeyError: print('Invalid url: "{}". Use urls from https://play.qobuz.com!'.format(url))
print("Invalid url. Use urls from https://play.qobuz.com!")
return return
if type_dict["func"]: if type_dict["func"]:
items = [ content = [item for item in type_dict["func"](item_id)]
item[type_dict["iterable_key"]]["items"] content_name = content[0]["name"]
for item in type_dict["func"](item_id) print(
][0] "\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: for item in items:
fromUrl( fromUrl(
client, client,
item["id"], item["id"],
path, new_path,
quality, quality,
True if type_dict["iterable_key"] == "albums" else False, True if type_dict["iterable_key"] == "albums" else False,
embed_art,
) )
else: 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: while True:
Albums, Types, IDs = [], [], [] Albums, Types, IDs = [], [], []
try: try:
@ -180,19 +162,86 @@ def interactive(Qz, path, limit, tracks=True):
) )
Qualits = ["320", "Lossless", "Hi-res =< 96kHz", "Hi-Res > 96 kHz"] Qualits = ["320", "Lossless", "Hi-res =< 96kHz", "Hi-Res > 96 kHz"]
quality = pick(Qualits, desc, default_index=1) 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: except KeyboardInterrupt:
sys.exit("\nBye") 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(): def main():
if not os.path.isdir(CONFIG_PATH) or not os.path.isfile(CONFIG_FILE): if not os.path.isdir(CONFIG_PATH) or not os.path.isfile(CONFIG_FILE):
try: try:
os.mkdir(CONFIG_PATH) os.makedirs(CONFIG_PATH, exist_ok=True)
except FileExistsError: except FileExistsError:
pass pass
reset_config(CONFIG_FILE) reset_config(CONFIG_FILE)
if len(sys.argv) < 2:
sys.exit(qobuz_dl_args().print_help())
email = None email = None
password = None password = None
app_id = None app_id = None
@ -211,21 +260,54 @@ def main():
secrets = [ secrets = [
secret for secret in config["DEFAULT"]["secrets"].split(",") if secret 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: except KeyError:
arguments = qobuz_dl_args().parse_args()
if not arguments.reset:
print("Your config file is corrupted! Run 'qobuz-dl -r' to fix this\n") print("Your config file is corrupted! Run 'qobuz-dl -r' to fix this\n")
arguments = getArgs() if arguments.reset:
if arguments.r:
sys.exit(reset_config(CONFIG_FILE)) sys.exit(reset_config(CONFIG_FILE))
directory = musicDir(arguments.d) + "/" directory = musicDir(arguments.directory)
Qz = qopy.Client(email, password, app_id, secrets) Qz = qopy.Client(email, password, app_id, secrets)
if not arguments.i: try:
interactive(Qz, directory, arguments.l, not arguments.a) 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: else:
handle_urls(arguments.i, Qz, directory, arguments.q) handle_urls(url, Qz, directory, arguments.quality, arguments.embed_art)
else:
download_lucky_mode(
Qz,
arguments.type,
" ".join(arguments.QUERY),
arguments.number,
directory,
arguments.quality,
arguments.embed_art,
)
if __name__ == "__main__": if __name__ == "__main__":

105
qobuz_dl/commands.py Normal file
View File

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

View File

@ -7,7 +7,7 @@ from tqdm import tqdm
import qobuz_dl.metadata as metadata 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) r = requests.get(url, allow_redirects=True, stream=True)
total = int(r.headers.get("content-length", 0)) total = int(r.headers.get("content-length", 0))
with open(fname, "wb") as file, tqdm( with open(fname, "wb") as file, tqdm(
@ -25,42 +25,115 @@ def req_tqdm(url, fname, track_name):
def mkDir(dirn): def mkDir(dirn):
try: try:
os.mkdir(dirn) os.makedirs(dirn, exist_ok=True)
except FileExistsError: except FileExistsError:
print("Warning: folder already exists. Overwriting...") pass
def getDesc(u, mt): def getDesc(u, mt, multiple=None):
return "{} [{}/{}]".format(mt["title"], u["bit_depth"], u["sampling_rate"]) return "{} [{}/{}]".format(
("[Disc {}] {}".format(multiple, mt["title"])) if multiple else mt["title"],
u["bit_depth"],
u["sampling_rate"],
)
def getBooklet(i, dirn): def get_format(album_dict, quality):
req_tqdm(i, dirn + "/booklet.pdf", "Downloading booklet") 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): def get_extra(i, dirn, extra="cover.jpg"):
req_tqdm(i, dirn + "/cover.jpg", "Downloading cover art") tqdm_download(i, os.path.join(dirn, extra), "Downloading " + extra.split(".")[0])
# Download and tag a file # Download and tag a file
def downloadItem(dirn, count, parse, meta, album, url, is_track, mp3): def download_and_tag(
fname = ( root_dir,
"{}/{:02}.mp3".format(dirn, count) tmp_count,
if mp3 track_url_dict,
else "{}/{:02}.flac".format(dirn, count) 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 final_file = os.path.join(root_dir, track_file)
desc = getDesc(parse, meta) if os.path.isfile(final_file):
req_tqdm(url, fname, desc) print(track_metadata["title"] + " was already downloaded. Skipping...")
func(fname, dirn, meta, album, is_track) 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 download_id_by_type(client, item_id, path, quality, album=False, embed_art=False):
def iterateIDs(client, id, path, quality, album=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 count = 0
if album: if album:
meta = client.get_album_meta(id) meta = client.get_album_meta(item_id)
album_title = ( album_title = (
"{} ({})".format(meta["title"], meta["version"]) "{} ({})".format(meta["title"], meta["version"])
if meta["version"] if meta["version"]
@ -71,35 +144,42 @@ def iterateIDs(client, id, path, quality, album=False):
meta["artist"]["name"], meta["artist"]["name"],
album_title, album_title,
meta["release_date_original"].split("-")[0], meta["release_date_original"].split("-")[0],
get_format(meta, quality),
) )
sanitized_title = sanitize_filename("{} - {} [{}]".format(*dirT)) sanitized_title = sanitize_filename("{} - {} [{}] [{}]".format(*dirT))
dirn = path + sanitized_title dirn = os.path.join(path, sanitized_title)
mkDir(dirn) mkDir(dirn)
getCover(meta["image"]["large"], dirn) get_extra(meta["image"]["large"], dirn)
if "goodies" in meta: if "goodies" in meta:
try: try:
getBooklet(meta["goodies"][0]["url"], dirn) get_extra(meta["goodies"][0]["url"], dirn, "booklet.pdf")
except Exception as e: except Exception as e:
print("Error: " + 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"]: for i in meta["tracks"]["items"]:
parse = client.get_track_url(i["id"], quality) parse = client.get_track_url(i["id"], quality)
try: if "sample" not in parse and parse["sampling_rate"]:
url = parse["url"]
except KeyError:
print("Track is not available for download")
return
if "sample" not in parse:
is_mp3 = True if int(quality) == 5 else False 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: else:
print("Demo. Skipping") print("Demo. Skipping")
count = count + 1 count = count + 1
else: else:
parse = client.get_track_url(id, quality) parse = client.get_track_url(item_id, quality)
url = parse["url"]
if "sample" not in parse: if "sample" not in parse and parse["sampling_rate"]:
meta = client.get_track_meta(id) meta = client.get_track_meta(item_id)
track_title = ( track_title = (
"{} ({})".format(meta["title"], meta["version"]) "{} ({})".format(meta["title"], meta["version"])
if meta["version"] if meta["version"]
@ -110,13 +190,14 @@ def iterateIDs(client, id, path, quality, album=False):
meta["album"]["artist"]["name"], meta["album"]["artist"]["name"],
track_title, track_title,
meta["album"]["release_date_original"].split("-")[0], meta["album"]["release_date_original"].split("-")[0],
get_format(meta, quality),
) )
sanitized_title = sanitize_filename("{} - {} [{}]".format(*dirT)) sanitized_title = sanitize_filename("{} - {} [{}] [{}]".format(*dirT))
dirn = path + sanitized_title dirn = os.path.join(path, sanitized_title)
mkDir(dirn) mkDir(dirn)
getCover(meta["album"]["image"]["large"], dirn) get_extra(meta["album"]["image"]["large"], dirn)
is_mp3 = True if int(quality) == 5 else False 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: else:
print("Demo. Skipping") print("Demo. Skipping")
print("\nCompleted\n") print("\nCompleted\n")

View File

@ -1,12 +1,22 @@
import os import os
from mutagen.flac import FLAC from mutagen.flac import FLAC, Picture
from mutagen.mp3 import EasyMP3 from mutagen.mp3 import EasyMP3
from pathvalidate import sanitize_filename
def tag_flac(file, path, d, album, istrack=True): def tag_flac(filename, root_dir, final_name, d, album, istrack=True, em_image=False):
audio = FLAC(file) """
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"] = ( audio["TITLE"] = (
"{} ({})".format(d["title"], d["version"]) if d["version"] else d["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["ALBUM"] = album["title"] # ALBUM TITLE
audio["YEAR"] = album["release_date_original"].split("-")[0] # YEAR audio["YEAR"] = album["release_date_original"].split("-")[0] # YEAR
audio.save() emb_image = os.path.join(root_dir, "cover.jpg")
title = sanitize_filename(d["title"]) if os.path.isfile(emb_image) and em_image:
try: try:
os.rename(file, "{}/{:02}. {}.flac".format(path, d["track_number"], title)) image = Picture()
except FileExistsError: image.type = 3
print("File already exists. Skipping...") 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()
os.rename(filename, final_name)
def tag_mp3(file, path, d, album, istrack=True): def tag_mp3(filename, root_dir, final_name, d, album, istrack=True, em_image=False):
audio = EasyMP3(file) """
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"] = ( audio["title"] = (
"{} ({})".format(d["title"], d["version"]) if d["version"] else d["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["date"] = album["release_date_original"].split("-")[0] # YEAR
audio.save() audio.save()
title = sanitize_filename(d["title"]) os.rename(filename, final_name)
try:
os.rename(file, "{}/{:02}. {}.mp3".format(path, d["track_number"], title))
except FileExistsError:
print("File already exists. Skipping...")

View File

@ -115,7 +115,7 @@ class Client:
self.uat = usr_info["user_auth_token"] self.uat = usr_info["user_auth_token"]
self.session.headers.update({"X-User-Auth-Token": self.uat}) self.session.headers.update({"X-User-Auth-Token": self.uat})
self.label = usr_info["user"]["credential"]["parameters"]["short_label"] 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): def multi_meta(self, epoint, key, id, type):
total = 1 total = 1
@ -154,6 +154,12 @@ class Client:
def search_albums(self, query, limit): def search_albums(self, query, limit):
return self.api_call("album/search", query=query, limit=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): def search_tracks(self, query, limit):
return self.api_call("track/search", query=query, limit=limit) return self.api_call("track/search", query=query, limit=limit)

View File

@ -8,6 +8,7 @@ class Search:
self.Types = [] self.Types = []
self.Tracks = Qz.search_tracks(query, limit)["tracks"]["items"] self.Tracks = Qz.search_tracks(query, limit)["tracks"]["items"]
self.Albums = Qz.search_albums(query, limit)["albums"]["items"] self.Albums = Qz.search_albums(query, limit)["albums"]["items"]
self.Artists = Qz.search_artists(query, limit)
def seconds(self, duration): def seconds(self, duration):
return time.strftime("%H:%M:%S", time.gmtime(duration)) return time.strftime("%H:%M:%S", time.gmtime(duration))

View File

@ -15,7 +15,7 @@ if os.name == "nt":
setup( setup(
name=pkg_name, name=pkg_name,
version="0.4.2", version="0.5",
author="Vitiko", author="Vitiko",
author_email="vhnz98@gmail.com", author_email="vhnz98@gmail.com",
description="The complete Lossless and Hi-Res music downloader for Qobuz", description="The complete Lossless and Hi-Res music downloader for Qobuz",
@ -38,5 +38,6 @@ setup(
python_requires=">=3.6", python_requires=">=3.6",
) )
# python setup.py sdist # python3 setup.py sdist bdist_wheel
# rm -f dist/*
# twine upload dist/* # twine upload dist/*