Partial rewrite

* Fix #26
* Fix #25
* Fix #23 (new flag: --albums-only)
* Fix #6
* Add support for last.fm playlists
* Update README
This commit is contained in:
vitiko98 2020-12-14 19:48:15 -04:00
parent 5525a48a9b
commit 4179f3bc73
8 changed files with 456 additions and 323 deletions

View File

@ -1,14 +1,15 @@
# qobuz-dl
Seach and download Lossless and Hi-Res music from [Qobuz](https://www.qobuz.com/).
Search, discover and download Lossless and Hi-Res music from [Qobuz](https://www.qobuz.com/).
## Features
* Download FLAC and MP3 files from Qobuz
* Search and download music directly from your terminal with **interactive** or **lucky** mode
* Explore and download music directly from your terminal with **interactive** or **lucky** mode
* Download albums, tracks, artists, playlists and labels with **download** mode
* Download music from last.fm playlists (Spotify, Apple Music and Youtube playlists are also supported through this method)
* Queue support on **interactive** mode
* Support for albums with multiple discs
* Read URLs from text file
* Downloads URLs from text file
* And more
## Getting started
@ -56,10 +57,22 @@ Download albums from a label and also embed cover art images into the downloaded
```
qobuz-dl dl https://play.qobuz.com/label/7526 --embed-art
```
Download a playlist in maximum quality
Download a Qobuz playlist in maximum quality
```
qobuz-dl dl https://play.qobuz.com/playlist/5388296 -q 27
```
Download all the music from an artist except singles, EPs and VA releases
```
qobuz-dl dl https://play.qobuz.com/artist/2528676 --albums-only
```
#### Last.fm playlists
Last.fm has a new feature for creating playlists: you can create your own based on the music you listen or you can import one from popular streaming services like Spotify, Apple Music and Youtube. Visit: `https://www.last.fm/user/<your profile>/playlists` (e.g. https://www.last.fm/user/vitiko98/playlists) to get started.
Download a last.fm playlist in the maximum quality
```
qobuz-dl dl https://www.last.fm/user/vitiko98/playlists/11887574 -q 27
```
Run `qobuz-dl dl --help` for more info.
@ -124,7 +137,27 @@ commands:
dl input mode
lucky lucky mode
```
## Module usage
Using `qobuz-dl` as a module is really easy. Basically, the only thing you need is to initialize `QobuzDL` from `core`.
```python
from qobuz_dl.core import QobuzDL
email = "your@email.com"
password = "your_password"
qobuz = QobuzDL()
qobuz.get_tokens() # get 'app_id' and 'secrets' attrs
qobuz.initialize_client(email, password, qobuz.app_id, qobuz.secrets)
qobuz.handle_url("https://play.qobuz.com/album/va4j3hdlwaubc")
```
Attributes, methods and parameters have been named as self-explanatory as possible.
## A note about Qo-DL
`qobuz-dl` is inspired in the discontinued Qo-DL-Reborn. This program uses two modules from Qo-DL: `qopy` and `spoofer`, both written by Sorrow446 and DashLt.
## Disclaimer
This tool was written for educational purposes. I will not be responsible if you use this program in bad faith. By using it, you are accepting the [Qobuz API Terms of Use](https://static.qobuz.com/apps/api/QobuzAPI-TermsofUse.pdf).
* This tool was written for educational purposes. I will not be responsible if you use this program in bad faith. By using it, you are accepting the [Qobuz API Terms of Use](https://static.qobuz.com/apps/api/QobuzAPI-TermsofUse.pdf).
* `qobuz-dl` is not affiliated with Qobuz

View File

@ -1,16 +1,10 @@
import argparse
import base64
import configparser
import os
import re
import sys
from pick import pick
from pathvalidate import sanitize_filename
import qobuz_dl.spoofbuz as spoofbuz
from qobuz_dl import downloader, qopy
from qobuz_dl.search import Search
from qobuz_dl.core import QobuzDL
from qobuz_dl.commands import qobuz_dl_args
if os.name == "nt":
@ -21,8 +15,6 @@ else:
CONFIG_PATH = os.path.join(OS_CONFIG, "qobuz-dl")
CONFIG_FILE = os.path.join(CONFIG_PATH, "config.ini")
QUALITIES = {5: "320", 6: "LOSSLESS", 7: "24B <96KHZ", 27: "24B <196KHZ"}
def reset_config(config_file):
print("Creating config file: " + config_file)
@ -43,7 +35,7 @@ def reset_config(config_file):
)
or "6"
)
config["DEFAULT"]["default_limit"] = "10"
config["DEFAULT"]["default_limit"] = "20"
print("Getting tokens. Please wait...")
spoofer = spoofbuz.Spoofer()
config["DEFAULT"]["app_id"] = str(spoofer.getAppId())
@ -53,193 +45,9 @@ def reset_config(config_file):
print("Config file updated.")
def musicDir(directory):
fix = os.path.normpath(directory)
if not os.path.isdir(fix):
print("New directory created: " + fix)
os.makedirs(fix, exist_ok=True)
return fix
def get_id(url):
return re.match(
r"https?://(?:w{0,3}|play|open)\.qobuz\.com/(?:(?:album|track|artist"
"|playlist|label)/|[a-z]{2}-[a-z]{2}/album/-?\w+(?:-\w+)*-?/|user/library/favorites/)(\w+)",
url,
).group(1)
def processSelected(Qz, path, albums, ids, types, quality, embed_art=False):
quality = [i for i in QUALITIES.keys()][quality[1]]
for alb, id_, type_ in zip(albums, ids, types):
for al in alb:
downloader.download_id_by_type(
Qz,
id_[al[1]],
path,
quality,
True if type_[al[1]] else False,
embed_art,
)
def fromUrl(Qz, id, path, quality, album=True, embed_art=False):
downloader.download_id_by_type(Qz, id, path, str(quality), album, embed_art)
def handle_urls(url, client, path, quality, embed_art=False):
possibles = {
"playlist": {"func": client.get_plist_meta, "iterable_key": "tracks"},
"artist": {"func": client.get_artist_meta, "iterable_key": "albums"},
"label": {"func": client.get_label_meta, "iterable_key": "albums"},
"album": {"album": True, "func": None, "iterable_key": None},
"track": {"album": False, "func": None, "iterable_key": None},
}
try:
url_type = url.split("/")[3]
type_dict = possibles[url_type]
item_id = get_id(url)
except (KeyError, IndexError):
print('Invalid url: "{}". Use urls from https://play.qobuz.com!'.format(url))
return
if type_dict["func"]:
content = [item for item in type_dict["func"](item_id)]
content_name = content[0]["name"]
print(
"\nDownloading all the music from {} ({})!".format(content_name, url_type)
)
new_path = musicDir(os.path.join(path, sanitize_filename(content_name)))
items = [item[type_dict["iterable_key"]]["items"] for item in content][0]
for item in items:
fromUrl(
client,
item["id"],
new_path,
quality,
True if type_dict["iterable_key"] == "albums" else False,
embed_art,
)
else:
fromUrl(client, item_id, path, quality, type_dict["album"], embed_art)
def interactive(Qz, path, limit, tracks=True, embed_art=False):
while True:
Albums, Types, IDs = [], [], []
try:
while True:
query = input("\nEnter your search: [Ctrl + c to quit]\n- ")
print("Searching...")
if len(query.strip()) == 0:
break
start = Search(Qz, query, limit)
start.getResults(tracks)
if len(start.Total) == 0:
break
Types.append(start.Types)
IDs.append(start.IDs)
title = (
"Select [space] the item(s) you want to download "
"(zero or more)\nPress Ctrl + c to quit\n"
)
Selected = pick(
start.Total, title, multiselect=True, min_selection_count=0
)
if len(Selected) > 0:
Albums.append(Selected)
y_n = pick(
["Yes", "No"],
"Items were added to queue to be downloaded. Keep searching?",
)
if y_n[0][0] == "N":
break
else:
break
if len(Albums) > 0:
desc = (
"Select [intro] the quality (the quality will be automat"
"ically\ndowngraded if the selected is not found)"
)
Qualits = ["320", "Lossless", "Hi-res =< 96kHz", "Hi-Res > 96 kHz"]
quality = pick(Qualits, desc, default_index=1)
processSelected(Qz, path, Albums, IDs, Types, quality, embed_art)
except KeyboardInterrupt:
sys.exit("\nBye")
def download_by_txt_file(Qz, txt_file, path, quality, embed_art=False):
with open(txt_file, "r") as txt:
try:
urls = txt.read().strip().split()
except Exception as e:
print("Invalid text file: " + str(e))
return
print(
'qobuz-dl will download {} urls from file: "{}"\n'.format(
len(urls), txt_file
)
)
for url in urls:
handle_urls(url, Qz, path, quality, embed_art)
def download_lucky_mode(Qz, mode, query, limit, path, quality, embed_art=False):
if len(query) < 3:
sys.exit("Your search query is too short or invalid!")
print(
'Searching {}s for "{}".\n'
"qobuz-dl will attempt to download the first {} results.".format(
mode, query, limit
)
)
WEB_URL = "https://play.qobuz.com/"
possibles = {
"album": {
"func": Qz.search_albums,
"album": True,
"key": "albums",
},
"artist": {
"func": Qz.search_artists,
"album": True,
"key": "artists",
},
"track": {
"func": Qz.search_tracks,
"album": False,
"key": "tracks",
},
"playlist": {
"func": Qz.search_playlists,
"album": False,
"key": "playlists",
},
}
try:
mode_dict = possibles[mode]
results = mode_dict["func"](query, limit)
iterable = results[mode_dict["key"]]["items"]
# Use handle_urls as everything is already handled there :p
urls = ["{}{}/{}".format(WEB_URL, mode, i["id"]) for i in iterable]
print("Found {} results!".format(len(urls)))
for url in urls:
handle_urls(url, Qz, path, quality, embed_art)
except (KeyError, IndexError):
sys.exit("Invalid mode: " + str(mode))
def main():
if not os.path.isdir(CONFIG_PATH) or not os.path.isfile(CONFIG_FILE):
try:
os.makedirs(CONFIG_PATH, exist_ok=True)
except FileExistsError:
pass
os.makedirs(CONFIG_PATH, exist_ok=True)
reset_config(CONFIG_FILE)
if len(sys.argv) < 2:
@ -273,44 +81,24 @@ def main():
if arguments.reset:
sys.exit(reset_config(CONFIG_FILE))
directory = musicDir(arguments.directory)
qobuz = QobuzDL(
arguments.directory,
arguments.quality,
arguments.embed_art,
ignore_singles_eps=arguments.albums_only,
)
qobuz.initialize_client(email, password, app_id, secrets)
Qz = qopy.Client(email, password, app_id, secrets)
try:
quality_str = QUALITIES[int(arguments.quality)]
print("Quality set: " + quality_str)
except KeyError:
sys.exit("Invalid quality!")
if arguments.command == "fun":
sys.exit(
interactive(
Qz,
directory,
arguments.limit,
not arguments.albums_only,
arguments.embed_art,
)
)
if arguments.command == "dl":
for url in arguments.SOURCE:
if os.path.isfile(url):
download_by_txt_file(
Qz, url, directory, arguments.quality, arguments.embed_art
)
else:
handle_urls(url, Qz, directory, arguments.quality, arguments.embed_art)
qobuz.download_list_of_urls(arguments.SOURCE)
elif arguments.command == "lucky":
query = " ".join(arguments.QUERY)
qobuz.lucky_type = arguments.type
qobuz.lucky_limit = arguments.number
qobuz.lucky_mode(query)
else:
download_lucky_mode(
Qz,
arguments.type,
" ".join(arguments.QUERY),
arguments.number,
directory,
arguments.quality,
arguments.embed_art,
)
qobuz.interactive_limit = arguments.limit
qobuz.interactive()
if __name__ == "__main__":

View File

@ -7,18 +7,12 @@ def fun_args(subparsers, default_limit):
description="Interactively search for tracks and albums.",
help="interactive mode",
)
interactive.add_argument(
"-a",
"--albums-only",
action="store_true",
help="enable albums-only search",
)
interactive.add_argument(
"-l",
"--limit",
metavar="int",
default=default_limit,
help="limit of search results by type (default: 10)",
help="limit of search results (default: 20)",
)
return interactive
@ -49,7 +43,7 @@ def lucky_args(subparsers):
def dl_args(subparsers):
download = subparsers.add_parser(
"dl",
description="Download by album/track/artist/label/playlist URL.",
description="Download by album/track/artist/label/playlist/last.fm-playlist URL.",
help="input mode",
)
download.add_argument(
@ -82,10 +76,15 @@ def add_common_arg(custom_parser, default_folder, default_quality):
"[320, LOSSLESS, 24B <96KHZ, 24B >96KHZ] (default: 6)"
),
)
custom_parser.add_argument(
"--albums-only",
action="store_true",
help=("don't download singles, EPs and VA releases"),
)
def qobuz_dl_args(
default_quality=6, default_limit=10, default_folder="Qobuz Downloads"
default_quality=6, default_limit=20, default_folder="Qobuz Downloads"
):
parser = argparse.ArgumentParser(
prog="qobuz-dl",

350
qobuz_dl/core.py Normal file
View File

@ -0,0 +1,350 @@
import os
import re
import string
import sys
import time
import requests
from bs4 import BeautifulSoup as bso
from pathvalidate import sanitize_filename
import qobuz_dl.spoofbuz as spoofbuz
from qobuz_dl import downloader, qopy
WEB_URL = "https://play.qobuz.com/"
ARTISTS_SELECTOR = "td.chartlist-artist > a"
TITLE_SELECTOR = "td.chartlist-name > a"
class PartialFormatter(string.Formatter):
def __init__(self, missing="n/a", bad_fmt="n/a"):
self.missing, self.bad_fmt = missing, bad_fmt
def get_field(self, field_name, args, kwargs):
try:
val = super(PartialFormatter, self).get_field(field_name, args, kwargs)
except (KeyError, AttributeError):
val = None, field_name
return val
def format_field(self, value, spec):
if not value:
return self.missing
try:
return super(PartialFormatter, self).format_field(value, spec)
except ValueError:
if self.bad_fmt:
return self.bad_fmt
raise
class QobuzDL:
def __init__(
self,
directory="Qobuz Downloads",
quality=6,
embed_art=False,
lucky_limit=1,
lucky_type="album",
interactive_limit=20,
ignore_singles_eps=False,
):
self.directory = self.create_dir(directory)
self.quality = quality
self.embed_art = embed_art
self.lucky_limit = lucky_limit
self.lucky_type = lucky_type
self.interactive_limit = interactive_limit
self.ignore_singles_eps = ignore_singles_eps
def initialize_client(self, email, pwd, app_id, secrets):
self.client = qopy.Client(email, pwd, app_id, secrets)
def get_tokens(self):
spoofer = spoofbuz.Spoofer()
self.app_id = spoofer.getAppId()
self.secrets = [
secret for secret in spoofer.getSecrets().values() if secret
] # avoid empty fields
def create_dir(self, directory=None):
fix = os.path.normpath(directory)
os.makedirs(fix, exist_ok=True)
return fix
def get_id(self, url):
return re.match(
r"https?://(?:w{0,3}|play|open)\.qobuz\.com/(?:(?:album|track|artist"
"|playlist|label)/|[a-z]{2}-[a-z]{2}/album/-?\w+(?:-\w+)*-?/|user/"
"library/favorites/)(\w+)",
url,
).group(1)
def download_from_id(self, item_id, album=True, alt_path=None):
downloader.download_id_by_type(
self.client,
item_id,
self.directory if not alt_path else alt_path,
str(self.quality),
album,
self.embed_art,
self.ignore_singles_eps,
)
def handle_url(self, url):
possibles = {
"playlist": {
"func": self.client.get_plist_meta,
"iterable_key": "tracks",
},
"artist": {
"func": self.client.get_artist_meta,
"iterable_key": "albums",
},
"label": {
"func": self.client.get_label_meta,
"iterable_key": "albums",
},
"album": {"album": True, "func": None, "iterable_key": None},
"track": {"album": False, "func": None, "iterable_key": None},
}
try:
url_type = url.split("/")[3]
type_dict = possibles[url_type]
item_id = self.get_id(url)
except (KeyError, IndexError):
print(
'Invalid url: "{}". Use urls from https://play.qobuz.com!'.format(url)
)
return
if type_dict["func"]:
content = [item for item in type_dict["func"](item_id)]
content_name = content[0]["name"]
print(
"\nDownloading all the music from {} ({})!".format(
content_name, url_type
)
)
new_path = self.create_dir(
os.path.join(self.directory, sanitize_filename(content_name))
)
items = [item[type_dict["iterable_key"]]["items"] for item in content][0]
print("{} downloads in queue".format(len(items)))
for item in items:
self.download_from_id(
item["id"],
True if type_dict["iterable_key"] == "albums" else False,
new_path,
)
else:
self.download_from_id(item_id, type_dict["album"])
def download_list_of_urls(self, urls):
if not urls or not isinstance(urls, list):
print("Nothing to download")
return
for url in urls:
if "last.fm" in url:
self.download_lastfm_pl(url)
else:
self.handle_url(url)
def download_from_txt_file(self, txt_file):
with open(txt_file, "r") as txt:
try:
urls = txt.read().strip().split()
except Exception as e:
print("Invalid text file: " + str(e))
return
print(
'qobuz-dl will download {} urls from file: "{}"\n'.format(
len(urls), txt_file
)
)
self.download_list_of_urls(urls)
def lucky_mode(self, query, download=True):
if len(query) < 3:
sys.exit("Your search query is too short or invalid!")
print(
'Searching {}s for "{}".\n'
"qobuz-dl will attempt to download the first {} results.".format(
self.lucky_type, query, self.lucky_limit
)
)
results = self.search_by_type(query, self.lucky_type, self.lucky_limit, True)
if download:
self.download_list_of_urls(results)
return results
def format_duration(self, duration):
return time.strftime("%H:%M:%S", time.gmtime(duration))
def search_by_type(self, query, item_type, limit=10, lucky=False):
if len(query) < 3:
print("Your search query is too short or invalid!")
return
possibles = {
"album": {
"func": self.client.search_albums,
"album": True,
"key": "albums",
"format": "{artist[name]} - {title}",
"requires_extra": True,
},
"artist": {
"func": self.client.search_artists,
"album": True,
"key": "artists",
"format": "{name} - ({albums_count} releases)",
"requires_extra": False,
},
"track": {
"func": self.client.search_tracks,
"album": False,
"key": "tracks",
"format": "{performer[name]} - {title}",
"requires_extra": True,
},
"playlist": {
"func": self.client.search_playlists,
"album": False,
"key": "playlists",
"format": "{name} - ({tracks_count} releases)",
"requires_extra": False,
},
}
try:
mode_dict = possibles[item_type]
results = mode_dict["func"](query, limit)
iterable = results[mode_dict["key"]]["items"]
item_list = []
for i in iterable:
fmt = PartialFormatter()
text = fmt.format(mode_dict["format"], **i)
if mode_dict["requires_extra"]:
text = "{} - {} [{}]".format(
text,
self.format_duration(i["duration"]),
"HI-RES" if i["hires_streamable"] else "LOSSLESS",
)
url = "{}{}/{}".format(WEB_URL, item_type, i.get("id", ""))
item_list.append({"text": text, "url": url} if not lucky else url)
return item_list
except (KeyError, IndexError):
print("Invalid mode: " + item_type)
return
def interactive(self, download=True):
try:
from pick import pick
except (ImportError, ModuleNotFoundError):
if os.name == "nt":
print('Please install curses with "pip3 install windows-curses"')
return
raise
qualities = [
{"q_string": "320", "q": 5},
{"q_string": "Lossless", "q": 6},
{"q_string": "Hi-res =< 96kHz", "q": 7},
{"q_string": "Hi-Res > 96 kHz", "q": 27},
]
def get_title_text(option):
return option.get("text")
def get_quality_text(option):
return option.get("q_string")
try:
item_types = ["Albums", "Tracks", "Artists", "Playlists"]
selected_type = pick(item_types, "I'll search for:\n[press Intro]")[0][
:-1
].lower()
print("Ok, we'll search for " + selected_type + "s")
final_url_list = []
while True:
query = input("\nEnter your search: [Ctrl + c to quit]\n- ")
print("Searching...")
options = self.search_by_type(
query, selected_type, self.interactive_limit
)
if not options:
print("Nothing found!")
continue
title = (
'*** RESULTS FOR "{}" ***\n\n'
"Select [space] the item(s) you want to download "
"(one or more)\nPress Ctrl + c to quit\n"
"Don't select anything to try another search".format(query.title())
)
selected_items = pick(
options,
title,
multiselect=True,
min_selection_count=0,
options_map_func=get_title_text,
)
if len(selected_items) > 0:
[final_url_list.append(i[0]["url"]) for i in selected_items]
y_n = pick(
["Yes", "No"],
"Items were added to queue to be downloaded. Keep searching?",
)
if y_n[0][0] == "N":
break
else:
print("\nOk, try again...")
continue
if final_url_list:
desc = (
"Select [intro] the quality (the quality will be automat"
"ically\ndowngraded if the selected is not found)"
)
self.quality = pick(
qualities,
desc,
default_index=1,
options_map_func=get_quality_text,
)[0]["q"]
if download:
self.download_list_of_urls(final_url_list)
return final_url_list
except KeyboardInterrupt:
print("\nBye")
return
def download_lastfm_pl(self, playlist_url):
# Apparently, last fm API doesn't have a playlist endpoint. If you
# find out that it has, please fix this!
r = requests.get(playlist_url)
soup = bso(r.content, "html.parser")
artists = [artist.text for artist in soup.select(ARTISTS_SELECTOR)]
titles = [title.text for title in soup.select(TITLE_SELECTOR)]
if len(artists) == len(titles) and artists:
track_list = [
artist + " " + title for artist, title in zip(artists, titles)
]
if not track_list:
print("Nothing found")
return
pl_title = sanitize_filename(soup.select_one("h1").text)
print("Downloading playlist: " + pl_title)
self.directory = os.path.join(self.directory, pl_title)
for i in track_list:
track_url = self.search_by_type(i, "track", 1, lucky=True)[0]
if track_url:
self.handle_url(track_url)

View File

@ -79,7 +79,11 @@ def get_title(item_dict):
def get_extra(i, dirn, extra="cover.jpg"):
tqdm_download(i, os.path.join(dirn, extra), "Downloading " + extra.split(".")[0])
tqdm_download(
i.replace("_600.", "_org."),
os.path.join(dirn, extra),
"Downloading " + extra.split(".")[0],
)
# Download and tag a file
@ -149,7 +153,9 @@ def download_and_tag(
os.remove(filename)
def download_id_by_type(client, item_id, path, quality, album=False, embed_art=False):
def download_id_by_type(
client, item_id, path, quality, album=False, embed_art=False, albums_only=False
):
"""
Download and get metadata by ID and type (album or track)
@ -157,12 +163,22 @@ def download_id_by_type(client, item_id, path, quality, album=False, embed_art=F
:param int item_id: Qobuz item id
:param str path: The root directory where the item will be downloaded
:param int quality: Audio quality (5, 6, 7, 27)
:param bool album
:param bool album: album type or not
:param embed_art album: Embed cover art into files
:param bool albums_only: Ignore Singles, EPs and VA releases
"""
count = 0
if album:
meta = client.get_album_meta(item_id)
if albums_only and (
meta.get("release_type") != "album"
or meta.get("artist").get("name") == "Various Artists"
):
print("Ignoring Single/EP/VA: " + meta.get("title", ""))
return
album_title = get_title(meta)
print("\nDownloading: {}\n".format(album_title))
dirT = (

View File

@ -4,6 +4,23 @@ from mutagen.flac import FLAC, Picture
from mutagen.mp3 import EasyMP3
def get_title(track_dict):
try:
title = (
("{} ({})".format(track_dict["title"], track_dict["version"]))
if track_dict["version"]
else track_dict["title"]
)
except KeyError:
title = track_dict["title"]
# for classical works
if track_dict.get("work"):
title = "{}: {}".format(track_dict["work"], title)
return title
def tag_flac(filename, root_dir, final_name, d, album, istrack=True, em_image=False):
"""
Tag a FLAC file
@ -18,19 +35,10 @@ def tag_flac(filename, root_dir, final_name, d, album, istrack=True, em_image=Fa
"""
audio = FLAC(filename)
try:
audio["TITLE"] = "{} ({})".format(d["title"], d["version"])
except KeyError:
audio["TITLE"] = d["title"]
audio["TITLE"] = get_title(d)
audio["TRACKNUMBER"] = str(d["track_number"]) # TRACK NUMBER
try:
if d["work"]: # not none
audio["WORK"] = d["work"]
except (KeyError, ValueError):
pass
try:
audio["COMPOSER"] = d["composer"]["name"] # COMPOSER
except KeyError:
@ -88,18 +96,9 @@ def tag_mp3(filename, root_dir, final_name, d, album, istrack=True, em_image=Fal
# TODO: add embedded cover art support for mp3
audio = EasyMP3(filename)
try:
audio["title"] = "{} ({})".format(d["title"], d["version"])
except KeyError:
audio["title"] = d["title"]
audio["title"] = get_title(d)
audio["tracknumber"] = str(d["track_number"])
try:
if d["work"]: # not none
audio["discsubtitle"] = d["work"]
except (KeyError, ValueError):
pass
try:
audio["composer"] = d["composer"]["name"]
except KeyError:
@ -118,7 +117,7 @@ def tag_mp3(filename, root_dir, final_name, d, album, istrack=True, em_image=Fal
audio["album"] = d["album"]["title"] # ALBUM TITLE
audio["date"] = d["album"]["release_date_original"].split("-")[0]
else:
audio["GENRE"] = ", ".join(album["genres_list"]) # GENRE
audio["genre"] = ", ".join(album["genres_list"]) # GENRE
audio["albumartist"] = album["artist"]["name"] # ALBUM ARTIST
audio["album"] = album["title"] # ALBUM TITLE
audio["date"] = album["release_date_original"].split("-")[0] # YEAR

View File

@ -1,49 +0,0 @@
import time
class Search:
def __init__(self, Qz, query, limit=10):
self.Total = []
self.IDs = []
self.Types = []
self.Tracks = Qz.search_tracks(query, limit)["tracks"]["items"]
self.Albums = Qz.search_albums(query, limit)["albums"]["items"]
self.Artists = Qz.search_artists(query, limit)
def seconds(self, duration):
return time.strftime("%H:%M:%S", time.gmtime(duration))
def appendInfo(self, i, bool):
self.IDs.append(i["id"])
self.Types.append(bool)
def itResults(self, iterable):
for i in iterable:
try:
items = (
i["artist"]["name"],
i["title"],
self.seconds(i["duration"]),
"HI-RES" if i["hires"] else "Lossless",
)
self.Total.append("[RELEASE] {} - {} - {} [{}]".format(*items))
self.appendInfo(i, True)
except KeyError:
try:
artist_field = i["performer"]["name"]
except KeyError:
print("Download: " + i["title"])
artist_field = i["composer"]["name"]
items = (
artist_field,
i["title"],
self.seconds(i["duration"]),
"HI-RES" if i["hires"] else "Lossless",
)
self.Total.append("[TRACK] {} - {} - {} [{}]".format(*items))
self.appendInfo(i, False)
def getResults(self, tracks=False):
self.itResults(self.Albums)
if tracks:
self.itResults(self.Tracks)

View File

@ -1,6 +1,4 @@
from setuptools import setup, find_packages
import sys
import os
pkg_name = "qobuz-dl"
@ -11,12 +9,11 @@ def read_file(fname):
requirements = read_file("requirements.txt").strip().split()
if os.name == "nt" or "win" in sys.platform:
requirements.append("windows-curses")
setup(
name=pkg_name,
version="0.5.4.2",
version="0.5.0",
author="Vitiko",
author_email="vhnz98@gmail.com",
description="The complete Lossless and Hi-Res music downloader for Qobuz",
@ -39,6 +36,6 @@ setup(
python_requires=">=3.6",
)
# python3 setup.py sdist bdist_wheel
# rm -f dist/*
# python3 setup.py sdist bdist_wheel
# twine upload dist/*