mirror of
https://github.com/Wojtek242/qobuz-dl.git
synced 2024-12-22 15:24:40 +01:00
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:
parent
c54eb71713
commit
621c609721
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,6 +12,7 @@ subtitle_search.py
|
||||
*.json
|
||||
*.txt
|
||||
*.db
|
||||
*.sh
|
||||
|
||||
*.txt
|
||||
|
||||
|
101
README.md
101
README.md
@ -6,9 +6,12 @@ If you need help or want to report a problem, join [qobuz-dl's discord server](h
|
||||
## Features
|
||||
|
||||
* Download FLAC and MP3 files from Qobuz
|
||||
* Search and download music directly from your terminal with interactive mode
|
||||
* Queue support
|
||||
* Input url mode with download support for albums, tracks, artists, playlists and labels
|
||||
* Search and download music directly from your terminal with **interactive** or **lucky** mode
|
||||
* Download albums, tracks, artists, playlists and labels with **download** mode
|
||||
* Queue support on **interactive** mode
|
||||
* Support for albums with multiple discs
|
||||
* Read URLs from text file
|
||||
* And more
|
||||
|
||||
## Getting started
|
||||
|
||||
@ -34,21 +37,93 @@ qobuz-dl.exe
|
||||
|
||||
> If something fails, run `qobuz-dl -r` to reset your config file.
|
||||
|
||||
## Examples
|
||||
### Interactive mode
|
||||
Run interactive mode with a limit of 10 results
|
||||
```
|
||||
qobuz-dl fun -l 10
|
||||
```
|
||||
Now you can search albums and tracks:
|
||||
```
|
||||
Logging...
|
||||
Logged: OK
|
||||
Membership: Studio
|
||||
|
||||
|
||||
Enter your search: [Ctrl + c to quit]
|
||||
- fka twigs magdalene
|
||||
```
|
||||
Everything else is interactive. Enjoy.
|
||||
|
||||
Run `qobuz-dl fun --help` for more info.
|
||||
|
||||
### Download mode
|
||||
Download URL in 24B<96khz quality
|
||||
```
|
||||
qobuz-dl dl https://play.qobuz.com/album/qxjbxh1dc3xyb -q 7
|
||||
```
|
||||
Download multiple URLs to custom directory
|
||||
```
|
||||
qobuz-dl dl https://play.qobuz.com/artist/2038380 https://play.qobuz.com/album/ip8qjy1m6dakc -d "Some pop from 2020"
|
||||
```
|
||||
Download multiple URLs from text file
|
||||
```
|
||||
qobuz-dl dl this_txt_file_has_urls.txt
|
||||
```
|
||||
Download albums from a label and also embed cover art images into the downloaded files
|
||||
```
|
||||
qobuz-dl dl https://play.qobuz.com/label/7526 --embed-art
|
||||
```
|
||||
Download a playlist in maximum quality
|
||||
```
|
||||
qobuz-dl dl https://play.qobuz.com/playlist/5388296 -q 27
|
||||
```
|
||||
|
||||
Run `qobuz-dl dl --help` for more info.
|
||||
|
||||
### Lucky mode
|
||||
Download the first album result
|
||||
```
|
||||
qobuz-dl lucky playboi carti die lit
|
||||
```
|
||||
Download the first 5 artist results
|
||||
```
|
||||
qobuz-dl lucky joy division -n 5 --type artist
|
||||
```
|
||||
Download the first 3 track results in 320 quality
|
||||
```
|
||||
qobuz-dl lucky eric dolphy remastered --type track -n 3 -q 5
|
||||
```
|
||||
|
||||
Run `qobuz-dl lucky --help` for more info.
|
||||
|
||||
### Other
|
||||
Reset your config file
|
||||
```
|
||||
qobuz-dl -r
|
||||
```
|
||||
|
||||
## Usage
|
||||
```
|
||||
usage: qobuz-dl [-h] [-a] [-r] [-i Album/track URL] [-q int] [-l int] [-d PATH]
|
||||
usage: qobuz-dl [-h] [-r] {fun,dl,lucky} ...
|
||||
|
||||
The ultimate Qobuz music downloader.
|
||||
See usage examples on https://github.com/vitiko98/qobuz-dl
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-r create/reset config file
|
||||
-a enable albums-only search
|
||||
-i album/track/artist/label/playlist URL run qobuz-dl on URL input mode (download by url)
|
||||
-q int quality (5, 6, 7, 27) (default: 6) [320, LOSSLESS, 24B <96KHZ, 24B >96KHZ]
|
||||
-l int limit of search results by type (default: 10)
|
||||
-d PATH custom directory for downloads (default: 'Qobuz Downloads')
|
||||
-h, --help show this help message and exit
|
||||
-r, --reset create/reset config file
|
||||
|
||||
commands:
|
||||
run qobuz-dl <command> --help for more info
|
||||
(e.g. qobuz-dl fun --help)
|
||||
|
||||
{fun,dl,lucky}
|
||||
fun interactive mode
|
||||
dl input mode
|
||||
lucky lucky mode
|
||||
```
|
||||
## A note about Qo-DL
|
||||
`qobuz-dl` is inspired in the discontinued Qo-DL-Reborn. This program uses two modules from Qo-DL: `qopy` and `spoofer`, both written by Sorrow446 and DashLt.
|
||||
## Disclaimer
|
||||
This tool was written for educational purposes. I will not be responsible if you use this program in bad faith.
|
||||
Also, you are accepting this: [Qobuz API Terms of Use](https://static.qobuz.com/apps/api/QobuzAPI-TermsofUse.pdf).
|
||||
This tool was written for educational purposes. I will not be responsible if you use this program in bad faith. By using it, you are accepting the [Qobuz API Terms of Use](https://static.qobuz.com/apps/api/QobuzAPI-TermsofUse.pdf).
|
||||
|
206
qobuz_dl/cli.py
206
qobuz_dl/cli.py
@ -5,10 +5,12 @@ import re
|
||||
import sys
|
||||
|
||||
from pick import pick
|
||||
from pathvalidate import sanitize_filename
|
||||
|
||||
import qobuz_dl.spoofbuz as spoofbuz
|
||||
from qobuz_dl import downloader, qopy
|
||||
from qobuz_dl.search import Search
|
||||
from qobuz_dl.commands import qobuz_dl_args
|
||||
|
||||
if os.name == "nt":
|
||||
OS_CONFIG = os.environ.get("APPDATA")
|
||||
@ -18,6 +20,8 @@ else:
|
||||
CONFIG_PATH = os.path.join(OS_CONFIG, "qobuz-dl")
|
||||
CONFIG_FILE = os.path.join(CONFIG_PATH, "config.ini")
|
||||
|
||||
QUALITIES = {5: "320", 6: "LOSSLESS", 7: "24B <96KHZ", 27: "24B <196KHZ"}
|
||||
|
||||
|
||||
def reset_config(config_file):
|
||||
print("Creating config file: " + config_file)
|
||||
@ -46,40 +50,11 @@ def reset_config(config_file):
|
||||
print("Config file updated.")
|
||||
|
||||
|
||||
def getArgs(default_quality=6, default_limit=10, default_folder="Qobuz Downloads"):
|
||||
parser = argparse.ArgumentParser(prog="qobuz-dl")
|
||||
parser.add_argument("-a", action="store_true", help="enable albums-only search")
|
||||
parser.add_argument("-r", action="store_true", help="create/reset config file")
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
metavar="album/track/artist/label/playlist URL",
|
||||
help="run qobuz-dl on URL input mode (download by url)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-q",
|
||||
metavar="int",
|
||||
default=default_quality,
|
||||
help="quality for url input mode (5, 6, 7, 27) (default: 6)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
metavar="int",
|
||||
default=default_limit,
|
||||
help="limit of search results by type (default: 10)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
metavar="PATH",
|
||||
default=default_folder,
|
||||
help="custom directory for downloads (default: '{}')".format(default_folder),
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def musicDir(dir):
|
||||
fix = os.path.normpath(dir)
|
||||
def musicDir(directory):
|
||||
fix = os.path.normpath(directory)
|
||||
if not os.path.isdir(fix):
|
||||
os.mkdir(fix)
|
||||
print("New directory created: " + fix)
|
||||
os.makedirs(fix, exist_ok=True)
|
||||
return fix
|
||||
|
||||
|
||||
@ -91,21 +66,25 @@ def get_id(url):
|
||||
).group(1)
|
||||
|
||||
|
||||
def processSelected(Qz, path, albums, ids, types, quality):
|
||||
q = ["5", "6", "7", "27"]
|
||||
quality = q[quality[1]]
|
||||
def processSelected(Qz, path, albums, ids, types, quality, embed_art=False):
|
||||
quality = [i for i in QUALITIES.keys()][quality[1]]
|
||||
for alb, id_, type_ in zip(albums, ids, types):
|
||||
for al in alb:
|
||||
downloader.iterateIDs(
|
||||
Qz, id_[al[1]], path, quality, True if type_[al[1]] else False
|
||||
downloader.download_id_by_type(
|
||||
Qz,
|
||||
id_[al[1]],
|
||||
path,
|
||||
quality,
|
||||
True if type_[al[1]] else False,
|
||||
embed_art,
|
||||
)
|
||||
|
||||
|
||||
def fromUrl(Qz, id, path, quality, album=True):
|
||||
downloader.iterateIDs(Qz, id, path, str(quality), album)
|
||||
def fromUrl(Qz, id, path, quality, album=True, embed_art=False):
|
||||
downloader.download_id_by_type(Qz, id, path, str(quality), album, embed_art)
|
||||
|
||||
|
||||
def handle_urls(url, client, path, quality):
|
||||
def handle_urls(url, client, path, quality, embed_art=False):
|
||||
possibles = {
|
||||
"playlist": {"func": client.get_plist_meta, "iterable_key": "tracks"},
|
||||
"artist": {"func": client.get_artist_meta, "iterable_key": "albums"},
|
||||
@ -117,28 +96,31 @@ def handle_urls(url, client, path, quality):
|
||||
url_type = url.split("/")[3]
|
||||
type_dict = possibles[url_type]
|
||||
item_id = get_id(url)
|
||||
print("Downloading {}...".format(url_type))
|
||||
except KeyError:
|
||||
print("Invalid url. Use urls from https://play.qobuz.com!")
|
||||
except (KeyError, IndexError):
|
||||
print('Invalid url: "{}". Use urls from https://play.qobuz.com!'.format(url))
|
||||
return
|
||||
if type_dict["func"]:
|
||||
items = [
|
||||
item[type_dict["iterable_key"]]["items"]
|
||||
for item in type_dict["func"](item_id)
|
||||
][0]
|
||||
content = [item for item in type_dict["func"](item_id)]
|
||||
content_name = content[0]["name"]
|
||||
print(
|
||||
"\nDownloading all the music from {} ({})!".format(content_name, url_type)
|
||||
)
|
||||
new_path = musicDir(os.path.join(path, sanitize_filename(content_name)))
|
||||
items = [item[type_dict["iterable_key"]]["items"] for item in content][0]
|
||||
for item in items:
|
||||
fromUrl(
|
||||
client,
|
||||
item["id"],
|
||||
path,
|
||||
new_path,
|
||||
quality,
|
||||
True if type_dict["iterable_key"] == "albums" else False,
|
||||
embed_art,
|
||||
)
|
||||
else:
|
||||
fromUrl(client, item_id, path, quality, type_dict["album"])
|
||||
fromUrl(client, item_id, path, quality, type_dict["album"], embed_art)
|
||||
|
||||
|
||||
def interactive(Qz, path, limit, tracks=True):
|
||||
def interactive(Qz, path, limit, tracks=True, embed_art=False):
|
||||
while True:
|
||||
Albums, Types, IDs = [], [], []
|
||||
try:
|
||||
@ -180,19 +162,86 @@ def interactive(Qz, path, limit, tracks=True):
|
||||
)
|
||||
Qualits = ["320", "Lossless", "Hi-res =< 96kHz", "Hi-Res > 96 kHz"]
|
||||
quality = pick(Qualits, desc, default_index=1)
|
||||
processSelected(Qz, path, Albums, IDs, Types, quality)
|
||||
processSelected(Qz, path, Albums, IDs, Types, quality, embed_art)
|
||||
except KeyboardInterrupt:
|
||||
sys.exit("\nBye")
|
||||
|
||||
|
||||
def download_by_txt_file(Qz, txt_file, path, quality, embed_art=False):
|
||||
with open(txt_file, "r") as txt:
|
||||
try:
|
||||
urls = txt.read().strip().split()
|
||||
except Exception as e:
|
||||
print("Invalid text file: " + str(e))
|
||||
return
|
||||
print(
|
||||
'qobuz-dl will download {} urls from file: "{}"\n'.format(
|
||||
len(urls), txt_file
|
||||
)
|
||||
)
|
||||
for url in urls:
|
||||
handle_urls(url, Qz, path, quality, embed_art)
|
||||
|
||||
|
||||
def download_lucky_mode(Qz, mode, query, limit, path, quality, embed_art=False):
|
||||
if len(query) < 3:
|
||||
sys.exit("Your search query is too short or invalid!")
|
||||
|
||||
print(
|
||||
'Searching {}s for "{}".\n'
|
||||
"qobuz-dl will attempt to download the first {} results.".format(
|
||||
mode, query, limit
|
||||
)
|
||||
)
|
||||
|
||||
WEB_URL = "https://play.qobuz.com/"
|
||||
possibles = {
|
||||
"album": {
|
||||
"func": Qz.search_albums,
|
||||
"album": True,
|
||||
"key": "albums",
|
||||
},
|
||||
"artist": {
|
||||
"func": Qz.search_artists,
|
||||
"album": True,
|
||||
"key": "artists",
|
||||
},
|
||||
"track": {
|
||||
"func": Qz.search_tracks,
|
||||
"album": False,
|
||||
"key": "tracks",
|
||||
},
|
||||
"playlist": {
|
||||
"func": Qz.search_playlists,
|
||||
"album": False,
|
||||
"key": "playlists",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
mode_dict = possibles[mode]
|
||||
results = mode_dict["func"](query, limit)
|
||||
iterable = results[mode_dict["key"]]["items"]
|
||||
# Use handle_urls as everything is already handled there :p
|
||||
urls = ["{}{}/{}".format(WEB_URL, mode, i["id"]) for i in iterable]
|
||||
print("Found {} results!".format(len(urls)))
|
||||
for url in urls:
|
||||
handle_urls(url, Qz, path, quality, embed_art)
|
||||
except (KeyError, IndexError):
|
||||
sys.exit("Invalid mode: " + str(mode))
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.isdir(CONFIG_PATH) or not os.path.isfile(CONFIG_FILE):
|
||||
try:
|
||||
os.mkdir(CONFIG_PATH)
|
||||
os.makedirs(CONFIG_PATH, exist_ok=True)
|
||||
except FileExistsError:
|
||||
pass
|
||||
reset_config(CONFIG_FILE)
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit(qobuz_dl_args().print_help())
|
||||
|
||||
email = None
|
||||
password = None
|
||||
app_id = None
|
||||
@ -211,21 +260,54 @@ def main():
|
||||
secrets = [
|
||||
secret for secret in config["DEFAULT"]["secrets"].split(",") if secret
|
||||
]
|
||||
arguments = getArgs(default_quality, default_limit, default_folder)
|
||||
arguments = qobuz_dl_args(
|
||||
default_quality, default_limit, default_folder
|
||||
).parse_args()
|
||||
except KeyError:
|
||||
print("Your config file is corrupted! Run 'qobuz-dl -r' to fix this\n")
|
||||
arguments = getArgs()
|
||||
|
||||
if arguments.r:
|
||||
arguments = qobuz_dl_args().parse_args()
|
||||
if not arguments.reset:
|
||||
print("Your config file is corrupted! Run 'qobuz-dl -r' to fix this\n")
|
||||
if arguments.reset:
|
||||
sys.exit(reset_config(CONFIG_FILE))
|
||||
|
||||
directory = musicDir(arguments.d) + "/"
|
||||
directory = musicDir(arguments.directory)
|
||||
|
||||
Qz = qopy.Client(email, password, app_id, secrets)
|
||||
|
||||
if not arguments.i:
|
||||
interactive(Qz, directory, arguments.l, not arguments.a)
|
||||
try:
|
||||
quality_str = QUALITIES[int(arguments.quality)]
|
||||
print("Quality set: " + quality_str)
|
||||
except KeyError:
|
||||
sys.exit("Invalid quality!")
|
||||
|
||||
if arguments.command == "fun":
|
||||
sys.exit(
|
||||
interactive(
|
||||
Qz,
|
||||
directory,
|
||||
arguments.limit,
|
||||
not arguments.albums_only,
|
||||
arguments.embed_art,
|
||||
)
|
||||
)
|
||||
if arguments.command == "dl":
|
||||
for url in arguments.SOURCE:
|
||||
if os.path.isfile(url):
|
||||
download_by_txt_file(
|
||||
Qz, url, directory, arguments.quality, arguments.embed_art
|
||||
)
|
||||
else:
|
||||
handle_urls(url, Qz, directory, arguments.quality, arguments.embed_art)
|
||||
else:
|
||||
handle_urls(arguments.i, Qz, directory, arguments.q)
|
||||
download_lucky_mode(
|
||||
Qz,
|
||||
arguments.type,
|
||||
" ".join(arguments.QUERY),
|
||||
arguments.number,
|
||||
directory,
|
||||
arguments.quality,
|
||||
arguments.embed_art,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
105
qobuz_dl/commands.py
Normal file
105
qobuz_dl/commands.py
Normal 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
|
@ -7,7 +7,7 @@ from tqdm import tqdm
|
||||
import qobuz_dl.metadata as metadata
|
||||
|
||||
|
||||
def req_tqdm(url, fname, track_name):
|
||||
def tqdm_download(url, fname, track_name):
|
||||
r = requests.get(url, allow_redirects=True, stream=True)
|
||||
total = int(r.headers.get("content-length", 0))
|
||||
with open(fname, "wb") as file, tqdm(
|
||||
@ -25,42 +25,115 @@ def req_tqdm(url, fname, track_name):
|
||||
|
||||
def mkDir(dirn):
|
||||
try:
|
||||
os.mkdir(dirn)
|
||||
os.makedirs(dirn, exist_ok=True)
|
||||
except FileExistsError:
|
||||
print("Warning: folder already exists. Overwriting...")
|
||||
pass
|
||||
|
||||
|
||||
def getDesc(u, mt):
|
||||
return "{} [{}/{}]".format(mt["title"], u["bit_depth"], u["sampling_rate"])
|
||||
def getDesc(u, mt, multiple=None):
|
||||
return "{} [{}/{}]".format(
|
||||
("[Disc {}] {}".format(multiple, mt["title"])) if multiple else mt["title"],
|
||||
u["bit_depth"],
|
||||
u["sampling_rate"],
|
||||
)
|
||||
|
||||
|
||||
def getBooklet(i, dirn):
|
||||
req_tqdm(i, dirn + "/booklet.pdf", "Downloading booklet")
|
||||
def get_format(album_dict, quality):
|
||||
try:
|
||||
if int(quality) == 5:
|
||||
return "MP3"
|
||||
if album_dict["maximum_bit_depth"] == 16 and int(quality) < 7:
|
||||
return "FLAC"
|
||||
except KeyError:
|
||||
return "Unknown"
|
||||
return "Hi-Res"
|
||||
|
||||
|
||||
def getCover(i, dirn):
|
||||
req_tqdm(i, dirn + "/cover.jpg", "Downloading cover art")
|
||||
def get_extra(i, dirn, extra="cover.jpg"):
|
||||
tqdm_download(i, os.path.join(dirn, extra), "Downloading " + extra.split(".")[0])
|
||||
|
||||
|
||||
# Download and tag a file
|
||||
def downloadItem(dirn, count, parse, meta, album, url, is_track, mp3):
|
||||
fname = (
|
||||
"{}/{:02}.mp3".format(dirn, count)
|
||||
if mp3
|
||||
else "{}/{:02}.flac".format(dirn, count)
|
||||
def download_and_tag(
|
||||
root_dir,
|
||||
tmp_count,
|
||||
track_url_dict,
|
||||
track_metadata,
|
||||
album_or_track_metadata,
|
||||
is_track,
|
||||
is_mp3,
|
||||
embed_art=False,
|
||||
multiple=None,
|
||||
):
|
||||
"""
|
||||
Download and tag a file
|
||||
|
||||
:param str root_dir: Root directory where the track will be stored
|
||||
:param int tmp_count: Temporal download file number
|
||||
:param dict track_url_dict: get_track_url dictionary from Qobuz client
|
||||
:param dict track_metadata: Track item dictionary from Qobuz client
|
||||
:param dict album_or_track_metadata: Album/track dictionary from Qobuz client
|
||||
:param bool is_track
|
||||
:param bool is_mp3
|
||||
:param bool embed_art: Embed cover art into file (FLAC-only)
|
||||
:param multiple: Multiple disc integer
|
||||
:type multiple: integer or None
|
||||
"""
|
||||
extension = ".mp3" if is_mp3 else ".flac"
|
||||
|
||||
try:
|
||||
url = track_url_dict["url"]
|
||||
except KeyError:
|
||||
print("Track not available for download")
|
||||
return
|
||||
|
||||
if multiple:
|
||||
root_dir = os.path.join(root_dir, "Disc " + str(multiple))
|
||||
mkDir(root_dir)
|
||||
|
||||
filename = os.path.join(root_dir, ".{:02}".format(tmp_count) + extension)
|
||||
|
||||
new_track_title = sanitize_filename(track_metadata["title"])
|
||||
track_file = "{:02}. {}{}".format(
|
||||
track_metadata["track_number"], new_track_title, extension
|
||||
)
|
||||
func = metadata.tag_mp3 if mp3 else metadata.tag_flac
|
||||
desc = getDesc(parse, meta)
|
||||
req_tqdm(url, fname, desc)
|
||||
func(fname, dirn, meta, album, is_track)
|
||||
final_file = os.path.join(root_dir, track_file)
|
||||
if os.path.isfile(final_file):
|
||||
print(track_metadata["title"] + " was already downloaded. Skipping...")
|
||||
return
|
||||
|
||||
desc = getDesc(track_url_dict, track_metadata, multiple)
|
||||
tqdm_download(url, filename, desc)
|
||||
tag_function = metadata.tag_mp3 if is_mp3 else metadata.tag_flac
|
||||
try:
|
||||
tag_function(
|
||||
filename,
|
||||
root_dir,
|
||||
final_file,
|
||||
track_metadata,
|
||||
album_or_track_metadata,
|
||||
is_track,
|
||||
embed_art,
|
||||
)
|
||||
except Exception as e:
|
||||
print("Error tagging the file: " + str(e))
|
||||
os.remove(filename)
|
||||
|
||||
|
||||
# Iterate over IDs by type calling downloadItem
|
||||
def iterateIDs(client, id, path, quality, album=False):
|
||||
def download_id_by_type(client, item_id, path, quality, album=False, embed_art=False):
|
||||
"""
|
||||
Download and get metadata by ID and type (album or track)
|
||||
|
||||
:param Qopy client: qopy Client
|
||||
:param int item_id: Qobuz item id
|
||||
:param str path: The root directory where the item will be downloaded
|
||||
:param int quality: Audio quality (5, 6, 7, 27)
|
||||
:param bool album
|
||||
"""
|
||||
count = 0
|
||||
|
||||
if album:
|
||||
meta = client.get_album_meta(id)
|
||||
meta = client.get_album_meta(item_id)
|
||||
album_title = (
|
||||
"{} ({})".format(meta["title"], meta["version"])
|
||||
if meta["version"]
|
||||
@ -71,35 +144,42 @@ def iterateIDs(client, id, path, quality, album=False):
|
||||
meta["artist"]["name"],
|
||||
album_title,
|
||||
meta["release_date_original"].split("-")[0],
|
||||
get_format(meta, quality),
|
||||
)
|
||||
sanitized_title = sanitize_filename("{} - {} [{}]".format(*dirT))
|
||||
dirn = path + sanitized_title
|
||||
sanitized_title = sanitize_filename("{} - {} [{}] [{}]".format(*dirT))
|
||||
dirn = os.path.join(path, sanitized_title)
|
||||
mkDir(dirn)
|
||||
getCover(meta["image"]["large"], dirn)
|
||||
get_extra(meta["image"]["large"], dirn)
|
||||
if "goodies" in meta:
|
||||
try:
|
||||
getBooklet(meta["goodies"][0]["url"], dirn)
|
||||
get_extra(meta["goodies"][0]["url"], dirn, "booklet.pdf")
|
||||
except Exception as e:
|
||||
print("Error: " + e)
|
||||
media_numbers = [track["media_number"] for track in meta["tracks"]["items"]]
|
||||
is_multiple = True if len([*{*media_numbers}]) > 1 else False
|
||||
for i in meta["tracks"]["items"]:
|
||||
parse = client.get_track_url(i["id"], quality)
|
||||
try:
|
||||
url = parse["url"]
|
||||
except KeyError:
|
||||
print("Track is not available for download")
|
||||
return
|
||||
if "sample" not in parse:
|
||||
if "sample" not in parse and parse["sampling_rate"]:
|
||||
is_mp3 = True if int(quality) == 5 else False
|
||||
downloadItem(dirn, count, parse, i, meta, url, False, is_mp3)
|
||||
download_and_tag(
|
||||
dirn,
|
||||
count,
|
||||
parse,
|
||||
i,
|
||||
meta,
|
||||
False,
|
||||
is_mp3,
|
||||
embed_art,
|
||||
i["media_number"] if is_multiple else None,
|
||||
)
|
||||
else:
|
||||
print("Demo. Skipping")
|
||||
count = count + 1
|
||||
else:
|
||||
parse = client.get_track_url(id, quality)
|
||||
url = parse["url"]
|
||||
parse = client.get_track_url(item_id, quality)
|
||||
|
||||
if "sample" not in parse:
|
||||
meta = client.get_track_meta(id)
|
||||
if "sample" not in parse and parse["sampling_rate"]:
|
||||
meta = client.get_track_meta(item_id)
|
||||
track_title = (
|
||||
"{} ({})".format(meta["title"], meta["version"])
|
||||
if meta["version"]
|
||||
@ -110,13 +190,14 @@ def iterateIDs(client, id, path, quality, album=False):
|
||||
meta["album"]["artist"]["name"],
|
||||
track_title,
|
||||
meta["album"]["release_date_original"].split("-")[0],
|
||||
get_format(meta, quality),
|
||||
)
|
||||
sanitized_title = sanitize_filename("{} - {} [{}]".format(*dirT))
|
||||
dirn = path + sanitized_title
|
||||
sanitized_title = sanitize_filename("{} - {} [{}] [{}]".format(*dirT))
|
||||
dirn = os.path.join(path, sanitized_title)
|
||||
mkDir(dirn)
|
||||
getCover(meta["album"]["image"]["large"], dirn)
|
||||
get_extra(meta["album"]["image"]["large"], dirn)
|
||||
is_mp3 = True if int(quality) == 5 else False
|
||||
downloadItem(dirn, count, parse, meta, meta, url, True, is_mp3)
|
||||
download_and_tag(dirn, count, parse, meta, meta, True, is_mp3, embed_art)
|
||||
else:
|
||||
print("Demo. Skipping")
|
||||
print("\nCompleted\n")
|
||||
|
@ -1,12 +1,22 @@
|
||||
import os
|
||||
|
||||
from mutagen.flac import FLAC
|
||||
from mutagen.flac import FLAC, Picture
|
||||
from mutagen.mp3 import EasyMP3
|
||||
from pathvalidate import sanitize_filename
|
||||
|
||||
|
||||
def tag_flac(file, path, d, album, istrack=True):
|
||||
audio = FLAC(file)
|
||||
def tag_flac(filename, root_dir, final_name, d, album, istrack=True, em_image=False):
|
||||
"""
|
||||
Tag a FLAC file
|
||||
|
||||
:param str filename: FLAC file path
|
||||
:param str root_dir: Root dir used to get the cover art
|
||||
:param str final_name: Final name of the FLAC file (complete path)
|
||||
:param dict d: Track dictionary from Qobuz_client
|
||||
:param dict album: Album dictionary from Qobuz_client
|
||||
:param bool istrack
|
||||
:param bool em_image: Embed cover art into file
|
||||
"""
|
||||
audio = FLAC(filename)
|
||||
|
||||
audio["TITLE"] = (
|
||||
"{} ({})".format(d["title"], d["version"]) if d["version"] else d["title"]
|
||||
@ -38,16 +48,37 @@ def tag_flac(file, path, d, album, istrack=True):
|
||||
audio["ALBUM"] = album["title"] # ALBUM TITLE
|
||||
audio["YEAR"] = album["release_date_original"].split("-")[0] # YEAR
|
||||
|
||||
emb_image = os.path.join(root_dir, "cover.jpg")
|
||||
if os.path.isfile(emb_image) and em_image:
|
||||
try:
|
||||
image = Picture()
|
||||
image.type = 3
|
||||
image.mime = "image/jpeg"
|
||||
image.desc = "cover"
|
||||
with open(emb_image, "rb") as img:
|
||||
image.data = img.read()
|
||||
audio.add_picture(image)
|
||||
except Exception as e:
|
||||
print("Error embedding image: " + str(e))
|
||||
|
||||
audio.save()
|
||||
title = sanitize_filename(d["title"])
|
||||
try:
|
||||
os.rename(file, "{}/{:02}. {}.flac".format(path, d["track_number"], title))
|
||||
except FileExistsError:
|
||||
print("File already exists. Skipping...")
|
||||
os.rename(filename, final_name)
|
||||
|
||||
|
||||
def tag_mp3(file, path, d, album, istrack=True):
|
||||
audio = EasyMP3(file)
|
||||
def tag_mp3(filename, root_dir, final_name, d, album, istrack=True, em_image=False):
|
||||
"""
|
||||
Tag a mp3 file
|
||||
|
||||
:param str filename: mp3 file path
|
||||
:param str root_dir: Root dir used to get the cover art
|
||||
:param str final_name: Final name of the mp3 file (complete path)
|
||||
:param dict d: Track dictionary from Qobuz_client
|
||||
:param dict album: Album dictionary from Qobuz_client
|
||||
:param bool istrack
|
||||
:param bool em_image: Embed cover art into file
|
||||
"""
|
||||
# TODO: add embedded cover art support for mp3
|
||||
audio = EasyMP3(filename)
|
||||
|
||||
audio["title"] = (
|
||||
"{} ({})".format(d["title"], d["version"]) if d["version"] else d["title"]
|
||||
@ -77,8 +108,4 @@ def tag_mp3(file, path, d, album, istrack=True):
|
||||
audio["date"] = album["release_date_original"].split("-")[0] # YEAR
|
||||
|
||||
audio.save()
|
||||
title = sanitize_filename(d["title"])
|
||||
try:
|
||||
os.rename(file, "{}/{:02}. {}.mp3".format(path, d["track_number"], title))
|
||||
except FileExistsError:
|
||||
print("File already exists. Skipping...")
|
||||
os.rename(filename, final_name)
|
||||
|
@ -115,7 +115,7 @@ class Client:
|
||||
self.uat = usr_info["user_auth_token"]
|
||||
self.session.headers.update({"X-User-Auth-Token": self.uat})
|
||||
self.label = usr_info["user"]["credential"]["parameters"]["short_label"]
|
||||
print("Membership: {}".format(self.label))
|
||||
print("Membership: {}\n".format(self.label))
|
||||
|
||||
def multi_meta(self, epoint, key, id, type):
|
||||
total = 1
|
||||
@ -154,6 +154,12 @@ class Client:
|
||||
def search_albums(self, query, limit):
|
||||
return self.api_call("album/search", query=query, limit=limit)
|
||||
|
||||
def search_artists(self, query, limit):
|
||||
return self.api_call("artist/search", query=query, limit=limit)
|
||||
|
||||
def search_playlists(self, query, limit):
|
||||
return self.api_call("playlist/search", query=query, limit=limit)
|
||||
|
||||
def search_tracks(self, query, limit):
|
||||
return self.api_call("track/search", query=query, limit=limit)
|
||||
|
||||
|
@ -8,6 +8,7 @@ class Search:
|
||||
self.Types = []
|
||||
self.Tracks = Qz.search_tracks(query, limit)["tracks"]["items"]
|
||||
self.Albums = Qz.search_albums(query, limit)["albums"]["items"]
|
||||
self.Artists = Qz.search_artists(query, limit)
|
||||
|
||||
def seconds(self, duration):
|
||||
return time.strftime("%H:%M:%S", time.gmtime(duration))
|
||||
|
5
setup.py
5
setup.py
@ -15,7 +15,7 @@ if os.name == "nt":
|
||||
|
||||
setup(
|
||||
name=pkg_name,
|
||||
version="0.4.2",
|
||||
version="0.5",
|
||||
author="Vitiko",
|
||||
author_email="vhnz98@gmail.com",
|
||||
description="The complete Lossless and Hi-Res music downloader for Qobuz",
|
||||
@ -38,5 +38,6 @@ setup(
|
||||
python_requires=">=3.6",
|
||||
)
|
||||
|
||||
# python setup.py sdist
|
||||
# python3 setup.py sdist bdist_wheel
|
||||
# rm -f dist/*
|
||||
# twine upload dist/*
|
||||
|
Loading…
Reference in New Issue
Block a user