mirror of
https://github.com/Wojtek242/qobuz-dl.git
synced 2024-11-22 11:05:25 +01:00
parent
1bf8bb16de
commit
fd260ee0f3
@ -1,11 +1,18 @@
|
|||||||
import base64
|
import base64
|
||||||
import configparser
|
import configparser
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import qobuz_dl.spoofbuz as spoofbuz
|
import qobuz_dl.spoofbuz as spoofbuz
|
||||||
from qobuz_dl.core import QobuzDL
|
from qobuz_dl.color import DF, GREEN, MAGENTA, RED, YELLOW
|
||||||
from qobuz_dl.commands import qobuz_dl_args
|
from qobuz_dl.commands import qobuz_dl_args
|
||||||
|
from qobuz_dl.core import QobuzDL
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(message)s",
|
||||||
|
)
|
||||||
|
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
OS_CONFIG = os.environ.get("APPDATA")
|
OS_CONFIG = os.environ.get("APPDATA")
|
||||||
@ -17,32 +24,34 @@ CONFIG_FILE = os.path.join(CONFIG_PATH, "config.ini")
|
|||||||
|
|
||||||
|
|
||||||
def reset_config(config_file):
|
def reset_config(config_file):
|
||||||
print("Creating config file: " + config_file)
|
logging.info(f"{YELLOW}Creating config file: {config_file}")
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
config["DEFAULT"]["email"] = input("\nEnter your email:\n- ")
|
config["DEFAULT"]["email"] = input(f"{MAGENTA}Enter your email:\n-{DF} ")
|
||||||
config["DEFAULT"]["password"] = base64.b64encode(
|
config["DEFAULT"]["password"] = base64.b64encode(
|
||||||
input("\nEnter your password\n- ").encode()
|
input(f"{MAGENTA}Enter your password\n-{DF} ").encode()
|
||||||
).decode()
|
).decode()
|
||||||
config["DEFAULT"]["default_folder"] = (
|
config["DEFAULT"]["default_folder"] = (
|
||||||
input("\nFolder for downloads (leave empy for default 'Qobuz Downloads')\n- ")
|
input(
|
||||||
|
f"{MAGENTA}Folder for downloads (leave empy for default 'Qobuz Downloads')\n-{DF} "
|
||||||
|
)
|
||||||
or "Qobuz Downloads"
|
or "Qobuz Downloads"
|
||||||
)
|
)
|
||||||
config["DEFAULT"]["default_quality"] = (
|
config["DEFAULT"]["default_quality"] = (
|
||||||
input(
|
input(
|
||||||
"\nDownload quality (5, 6, 7, 27) "
|
f"{MAGENTA}Download quality (5, 6, 7, 27) "
|
||||||
"[320, LOSSLESS, 24B <96KHZ, 24B >96KHZ]"
|
"[320, LOSSLESS, 24B <96KHZ, 24B >96KHZ]"
|
||||||
"\n(leave empy for default '6')\n- "
|
f"\n(leave empy for default '6')\n-{DF} "
|
||||||
)
|
)
|
||||||
or "6"
|
or "6"
|
||||||
)
|
)
|
||||||
config["DEFAULT"]["default_limit"] = "20"
|
config["DEFAULT"]["default_limit"] = "20"
|
||||||
print("Getting tokens. Please wait...")
|
logging.info(f"{YELLOW}Getting tokens. Please wait...")
|
||||||
spoofer = spoofbuz.Spoofer()
|
spoofer = spoofbuz.Spoofer()
|
||||||
config["DEFAULT"]["app_id"] = str(spoofer.getAppId())
|
config["DEFAULT"]["app_id"] = str(spoofer.getAppId())
|
||||||
config["DEFAULT"]["secrets"] = ",".join(spoofer.getSecrets().values())
|
config["DEFAULT"]["secrets"] = ",".join(spoofer.getSecrets().values())
|
||||||
with open(config_file, "w") as configfile:
|
with open(config_file, "w") as configfile:
|
||||||
config.write(configfile)
|
config.write(configfile)
|
||||||
print("Config file updated.")
|
logging.info(f"{GREEN}Config file updated.")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@ -77,7 +86,9 @@ def main():
|
|||||||
except (KeyError, UnicodeDecodeError):
|
except (KeyError, UnicodeDecodeError):
|
||||||
arguments = qobuz_dl_args().parse_args()
|
arguments = qobuz_dl_args().parse_args()
|
||||||
if not arguments.reset:
|
if not arguments.reset:
|
||||||
print("Your config file is corrupted! Run 'qobuz-dl -r' to fix this\n")
|
logging.warning(
|
||||||
|
f"{RED}Your config file is corrupted! Run 'qobuz-dl -r' to fix this"
|
||||||
|
)
|
||||||
if arguments.reset:
|
if arguments.reset:
|
||||||
sys.exit(reset_config(CONFIG_FILE))
|
sys.exit(reset_config(CONFIG_FILE))
|
||||||
|
|
||||||
@ -91,16 +102,22 @@ def main():
|
|||||||
)
|
)
|
||||||
qobuz.initialize_client(email, password, app_id, secrets)
|
qobuz.initialize_client(email, password, app_id, secrets)
|
||||||
|
|
||||||
if arguments.command == "dl":
|
try:
|
||||||
qobuz.download_list_of_urls(arguments.SOURCE)
|
if arguments.command == "dl":
|
||||||
elif arguments.command == "lucky":
|
qobuz.download_list_of_urls(arguments.SOURCE)
|
||||||
query = " ".join(arguments.QUERY)
|
elif arguments.command == "lucky":
|
||||||
qobuz.lucky_type = arguments.type
|
query = " ".join(arguments.QUERY)
|
||||||
qobuz.lucky_limit = arguments.number
|
qobuz.lucky_type = arguments.type
|
||||||
qobuz.lucky_mode(query)
|
qobuz.lucky_limit = arguments.number
|
||||||
else:
|
qobuz.lucky_mode(query)
|
||||||
qobuz.interactive_limit = arguments.limit
|
else:
|
||||||
qobuz.interactive()
|
qobuz.interactive_limit = arguments.limit
|
||||||
|
qobuz.interactive()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logging.info(
|
||||||
|
f"{RED}Interrupted by user\n{MAGENTA}Already downloaded items will "
|
||||||
|
"be skipped if you try to download the same releases again"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
13
qobuz_dl/color.py
Normal file
13
qobuz_dl/color.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from colorama import Style, Fore, init
|
||||||
|
|
||||||
|
init(autoreset=True)
|
||||||
|
|
||||||
|
DF = Style.NORMAL
|
||||||
|
BG = Style.BRIGHT
|
||||||
|
OFF = Style.DIM
|
||||||
|
RED = Fore.RED
|
||||||
|
BLUE = Fore.BLUE
|
||||||
|
GREEN = Fore.GREEN
|
||||||
|
YELLOW = Fore.YELLOW
|
||||||
|
CYAN = Fore.CYAN
|
||||||
|
MAGENTA = Fore.MAGENTA
|
@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
@ -6,18 +7,21 @@ import time
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
from bs4 import BeautifulSoup as bso
|
from bs4 import BeautifulSoup as bso
|
||||||
|
from mutagen.flac import FLAC
|
||||||
|
from mutagen.mp3 import EasyMP3
|
||||||
from pathvalidate import sanitize_filename
|
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.color import MAGENTA, OFF, RED, YELLOW, DF
|
||||||
from mutagen.flac import FLAC
|
|
||||||
from mutagen.mp3 import EasyMP3
|
|
||||||
|
|
||||||
WEB_URL = "https://play.qobuz.com/"
|
WEB_URL = "https://play.qobuz.com/"
|
||||||
ARTISTS_SELECTOR = "td.chartlist-artist > a"
|
ARTISTS_SELECTOR = "td.chartlist-artist > a"
|
||||||
TITLE_SELECTOR = "td.chartlist-name > a"
|
TITLE_SELECTOR = "td.chartlist-name > a"
|
||||||
EXTENSIONS = (".mp3", ".flac")
|
EXTENSIONS = (".mp3", ".flac")
|
||||||
|
QUALITIES = {5: "5 - MP3", 6: "6 - FLAC", 7: "7 - 24B<96kHz", 27: "27 - 24B>96kHz"}
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PartialFormatter(string.Formatter):
|
class PartialFormatter(string.Formatter):
|
||||||
@ -67,6 +71,7 @@ class QobuzDL:
|
|||||||
|
|
||||||
def initialize_client(self, email, pwd, app_id, secrets):
|
def initialize_client(self, email, pwd, app_id, secrets):
|
||||||
self.client = qopy.Client(email, pwd, app_id, secrets)
|
self.client = qopy.Client(email, pwd, app_id, secrets)
|
||||||
|
logger.info(f"{YELLOW}Set quality: {QUALITIES[int(self.quality)]}")
|
||||||
|
|
||||||
def get_tokens(self):
|
def get_tokens(self):
|
||||||
spoofer = spoofbuz.Spoofer()
|
spoofer = spoofbuz.Spoofer()
|
||||||
@ -83,8 +88,8 @@ class QobuzDL:
|
|||||||
def get_id(self, url):
|
def get_id(self, url):
|
||||||
return re.match(
|
return re.match(
|
||||||
r"https?://(?:w{0,3}|play|open)\.qobuz\.com/(?:(?:album|track|artist"
|
r"https?://(?:w{0,3}|play|open)\.qobuz\.com/(?:(?:album|track|artist"
|
||||||
"|playlist|label)/|[a-z]{2}-[a-z]{2}/album/-?\w+(?:-\w+)*-?/|user/"
|
r"|playlist|label)/|[a-z]{2}-[a-z]{2}/album/-?\w+(?:-\w+)*-?/|user/"
|
||||||
"library/favorites/)(\w+)",
|
r"library/favorites/)(\w+)",
|
||||||
url,
|
url,
|
||||||
).group(1)
|
).group(1)
|
||||||
|
|
||||||
@ -122,23 +127,21 @@ class QobuzDL:
|
|||||||
type_dict = possibles[url_type]
|
type_dict = possibles[url_type]
|
||||||
item_id = self.get_id(url)
|
item_id = self.get_id(url)
|
||||||
except (KeyError, IndexError):
|
except (KeyError, IndexError):
|
||||||
print(
|
logger.info(
|
||||||
'Invalid url: "{}". Use urls from https://play.qobuz.com!'.format(url)
|
f'{RED}Invalid url: "{url}". Use urls from https://play.qobuz.com!'
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if type_dict["func"]:
|
if type_dict["func"]:
|
||||||
content = [item for item in type_dict["func"](item_id)]
|
content = [item for item in type_dict["func"](item_id)]
|
||||||
content_name = content[0]["name"]
|
content_name = content[0]["name"]
|
||||||
print(
|
logger.info(
|
||||||
"\nDownloading all the music from {} ({})!".format(
|
f"{YELLOW}Downloading all the music from {content_name} ({url_type})!"
|
||||||
content_name, url_type
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
new_path = self.create_dir(
|
new_path = self.create_dir(
|
||||||
os.path.join(self.directory, sanitize_filename(content_name))
|
os.path.join(self.directory, sanitize_filename(content_name))
|
||||||
)
|
)
|
||||||
items = [item[type_dict["iterable_key"]]["items"] for item in content][0]
|
items = [item[type_dict["iterable_key"]]["items"] for item in content][0]
|
||||||
print("{} downloads in queue".format(len(items)))
|
logger.info(f"{YELLOW}{len(items)} downloads in queue")
|
||||||
for item in items:
|
for item in items:
|
||||||
self.download_from_id(
|
self.download_from_id(
|
||||||
item["id"],
|
item["id"],
|
||||||
@ -152,7 +155,7 @@ class QobuzDL:
|
|||||||
|
|
||||||
def download_list_of_urls(self, urls):
|
def download_list_of_urls(self, urls):
|
||||||
if not urls or not isinstance(urls, list):
|
if not urls or not isinstance(urls, list):
|
||||||
print("Nothing to download")
|
logger.info(f"{OFF}Nothing to download")
|
||||||
return
|
return
|
||||||
for url in urls:
|
for url in urls:
|
||||||
if "last.fm" in url:
|
if "last.fm" in url:
|
||||||
@ -167,24 +170,21 @@ class QobuzDL:
|
|||||||
try:
|
try:
|
||||||
urls = txt.read().strip().split()
|
urls = txt.read().strip().split()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Invalid text file: " + str(e))
|
logger.error(f"{RED}Invalid text file: {e}")
|
||||||
return
|
return
|
||||||
print(
|
logger.info(
|
||||||
'qobuz-dl will download {} urls from file: "{}"\n'.format(
|
f'{YELLOW}qobuz-dl will download {len(urls)} urls from file: "{txt_file}"'
|
||||||
len(urls), txt_file
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self.download_list_of_urls(urls)
|
self.download_list_of_urls(urls)
|
||||||
|
|
||||||
def lucky_mode(self, query, download=True):
|
def lucky_mode(self, query, download=True):
|
||||||
if len(query) < 3:
|
if len(query) < 3:
|
||||||
sys.exit("Your search query is too short or invalid!")
|
logger.info(f"{RED}Your search query is too short or invalid")
|
||||||
|
return
|
||||||
|
|
||||||
print(
|
logger.info(
|
||||||
'Searching {}s for "{}".\n'
|
f'{YELLOW}Searching {self.lucky_type}s for "{query}".\n'
|
||||||
"qobuz-dl will attempt to download the first {} results.".format(
|
f"{YELLOW}qobuz-dl will attempt to download the first {self.lucky_limit} results."
|
||||||
self.lucky_type, query, self.lucky_limit
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
results = self.search_by_type(query, self.lucky_type, self.lucky_limit, True)
|
results = self.search_by_type(query, self.lucky_type, self.lucky_limit, True)
|
||||||
|
|
||||||
@ -198,7 +198,7 @@ class QobuzDL:
|
|||||||
|
|
||||||
def search_by_type(self, query, item_type, limit=10, lucky=False):
|
def search_by_type(self, query, item_type, limit=10, lucky=False):
|
||||||
if len(query) < 3:
|
if len(query) < 3:
|
||||||
print("Your search query is too short or invalid!")
|
logger.info("{RED}Your search query is too short or invalid")
|
||||||
return
|
return
|
||||||
|
|
||||||
possibles = {
|
possibles = {
|
||||||
@ -252,7 +252,7 @@ class QobuzDL:
|
|||||||
item_list.append({"text": text, "url": url} if not lucky else url)
|
item_list.append({"text": text, "url": url} if not lucky else url)
|
||||||
return item_list
|
return item_list
|
||||||
except (KeyError, IndexError):
|
except (KeyError, IndexError):
|
||||||
print("Invalid mode: " + item_type)
|
logger.info(f"{RED}Invalid type: {item_type}")
|
||||||
return
|
return
|
||||||
|
|
||||||
def interactive(self, download=True):
|
def interactive(self, download=True):
|
||||||
@ -260,8 +260,9 @@ class QobuzDL:
|
|||||||
from pick import pick
|
from pick import pick
|
||||||
except (ImportError, ModuleNotFoundError):
|
except (ImportError, ModuleNotFoundError):
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
print('Please install curses with "pip3 install windows-curses"')
|
sys.exit(
|
||||||
return
|
'Please install curses with "pip3 install windows-curses" to continue'
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
qualities = [
|
qualities = [
|
||||||
@ -282,22 +283,22 @@ class QobuzDL:
|
|||||||
selected_type = pick(item_types, "I'll search for:\n[press Intro]")[0][
|
selected_type = pick(item_types, "I'll search for:\n[press Intro]")[0][
|
||||||
:-1
|
:-1
|
||||||
].lower()
|
].lower()
|
||||||
print("Ok, we'll search for " + selected_type + "s")
|
logger.info(f"{YELLOW}Ok, we'll search for {selected_type}s")
|
||||||
final_url_list = []
|
final_url_list = []
|
||||||
while True:
|
while True:
|
||||||
query = input("\nEnter your search: [Ctrl + c to quit]\n- ")
|
query = input(f"{MAGENTA}Enter your search: [Ctrl + c to quit]\n-{DF} ")
|
||||||
print("Searching...")
|
logger.info(f"{YELLOW}Searching...")
|
||||||
options = self.search_by_type(
|
options = self.search_by_type(
|
||||||
query, selected_type, self.interactive_limit
|
query, selected_type, self.interactive_limit
|
||||||
)
|
)
|
||||||
if not options:
|
if not options:
|
||||||
print("Nothing found!")
|
logger.info(f"{OFF}Nothing found")
|
||||||
continue
|
continue
|
||||||
title = (
|
title = (
|
||||||
'*** RESULTS FOR "{}" ***\n\n'
|
f'*** RESULTS FOR "{query.title()}" ***\n\n'
|
||||||
"Select [space] the item(s) you want to download "
|
"Select [space] the item(s) you want to download "
|
||||||
"(one or more)\nPress Ctrl + c to quit\n"
|
"(one or more)\nPress Ctrl + c to quit\n"
|
||||||
"Don't select anything to try another search".format(query.title())
|
"Don't select anything to try another search"
|
||||||
)
|
)
|
||||||
selected_items = pick(
|
selected_items = pick(
|
||||||
options,
|
options,
|
||||||
@ -315,12 +316,12 @@ class QobuzDL:
|
|||||||
if y_n[0][0] == "N":
|
if y_n[0][0] == "N":
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
print("\nOk, try again...")
|
logger.info(f"{YELLOW}Ok, try again...")
|
||||||
continue
|
continue
|
||||||
if final_url_list:
|
if final_url_list:
|
||||||
desc = (
|
desc = (
|
||||||
"Select [intro] the quality (the quality will be automat"
|
"Select [intro] the quality (the quality will "
|
||||||
"ically\ndowngraded if the selected is not found)"
|
"be automatically\ndowngraded if the selected is not found)"
|
||||||
)
|
)
|
||||||
self.quality = pick(
|
self.quality = pick(
|
||||||
qualities,
|
qualities,
|
||||||
@ -334,29 +335,36 @@ class QobuzDL:
|
|||||||
|
|
||||||
return final_url_list
|
return final_url_list
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nBye")
|
logger.info(f"{YELLOW}Bye")
|
||||||
return
|
return
|
||||||
|
|
||||||
def download_lastfm_pl(self, playlist_url):
|
def download_lastfm_pl(self, playlist_url):
|
||||||
# Apparently, last fm API doesn't have a playlist endpoint. If you
|
# Apparently, last fm API doesn't have a playlist endpoint. If you
|
||||||
# find out that it has, please fix this!
|
# find out that it has, please fix this!
|
||||||
r = requests.get(playlist_url)
|
try:
|
||||||
|
r = requests.get(playlist_url, timeout=10)
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.error(f"{RED}Playlist download failed: {e}")
|
||||||
|
return
|
||||||
soup = bso(r.content, "html.parser")
|
soup = bso(r.content, "html.parser")
|
||||||
artists = [artist.text for artist in soup.select(ARTISTS_SELECTOR)]
|
artists = [artist.text for artist in soup.select(ARTISTS_SELECTOR)]
|
||||||
titles = [title.text for title in soup.select(TITLE_SELECTOR)]
|
titles = [title.text for title in soup.select(TITLE_SELECTOR)]
|
||||||
|
|
||||||
|
track_list = []
|
||||||
if len(artists) == len(titles) and artists:
|
if len(artists) == len(titles) and artists:
|
||||||
track_list = [
|
track_list = [
|
||||||
artist + " " + title for artist, title in zip(artists, titles)
|
artist + " " + title for artist, title in zip(artists, titles)
|
||||||
]
|
]
|
||||||
|
|
||||||
if not track_list:
|
if not track_list:
|
||||||
print("Nothing found")
|
logger.info(f"{OFF}Nothing found")
|
||||||
return
|
return
|
||||||
|
|
||||||
pl_title = sanitize_filename(soup.select_one("h1").text)
|
pl_title = sanitize_filename(soup.select_one("h1").text)
|
||||||
pl_directory = os.path.join(self.directory, pl_title)
|
pl_directory = os.path.join(self.directory, pl_title)
|
||||||
print("Downloading playlist: {} ({} tracks)".format(pl_title, len(track_list)))
|
logger.info(
|
||||||
|
f"{YELLOW}Downloading playlist: {pl_title} ({len(track_list)} tracks)"
|
||||||
|
)
|
||||||
|
|
||||||
for i in track_list:
|
for i in track_list:
|
||||||
track_id = self.get_id(self.search_by_type(i, "track", 1, lucky=True)[0])
|
track_id = self.get_id(self.search_by_type(i, "track", 1, lucky=True)[0])
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@ -5,8 +6,10 @@ from pathvalidate import sanitize_filename
|
|||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
import qobuz_dl.metadata as metadata
|
import qobuz_dl.metadata as metadata
|
||||||
|
from qobuz_dl.color import OFF, GREEN, RED, YELLOW, CYAN
|
||||||
|
|
||||||
QL_DOWNGRADE = "FormatRestrictedByFormatAvailability"
|
QL_DOWNGRADE = "FormatRestrictedByFormatAvailability"
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def tqdm_download(url, fname, track_name):
|
def tqdm_download(url, fname, track_name):
|
||||||
@ -18,7 +21,7 @@ def tqdm_download(url, fname, track_name):
|
|||||||
unit_scale=True,
|
unit_scale=True,
|
||||||
unit_divisor=1024,
|
unit_divisor=1024,
|
||||||
desc=track_name,
|
desc=track_name,
|
||||||
bar_format="{n_fmt}/{total_fmt} /// {desc}",
|
bar_format=CYAN + "{n_fmt}/{total_fmt} /// {desc}",
|
||||||
) as bar:
|
) as bar:
|
||||||
for data in r.iter_content(chunk_size=1024):
|
for data in r.iter_content(chunk_size=1024):
|
||||||
size = file.write(data)
|
size = file.write(data)
|
||||||
@ -87,12 +90,12 @@ def get_title(item_dict):
|
|||||||
def get_extra(i, dirn, extra="cover.jpg"):
|
def get_extra(i, dirn, extra="cover.jpg"):
|
||||||
extra_file = os.path.join(dirn, extra)
|
extra_file = os.path.join(dirn, extra)
|
||||||
if os.path.isfile(extra_file):
|
if os.path.isfile(extra_file):
|
||||||
print(extra.split(".")[0].title() + " already downloaded")
|
logger.info(f"{OFF}{extra} was already downloaded")
|
||||||
return
|
return
|
||||||
tqdm_download(
|
tqdm_download(
|
||||||
i.replace("_600.", "_org."),
|
i.replace("_600.", "_org."),
|
||||||
extra_file,
|
extra_file,
|
||||||
"Downloading " + extra.split(".")[0],
|
extra,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -127,7 +130,7 @@ def download_and_tag(
|
|||||||
try:
|
try:
|
||||||
url = track_url_dict["url"]
|
url = track_url_dict["url"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
print("Track not available for download")
|
logger.info(f"{OFF}Track not available for download")
|
||||||
return
|
return
|
||||||
|
|
||||||
if multiple:
|
if multiple:
|
||||||
@ -142,7 +145,7 @@ def download_and_tag(
|
|||||||
)
|
)
|
||||||
final_file = os.path.join(root_dir, track_file)
|
final_file = os.path.join(root_dir, track_file)
|
||||||
if os.path.isfile(final_file):
|
if os.path.isfile(final_file):
|
||||||
print(track_metadata["title"] + " was already downloaded. Skipping...")
|
logger.info(f'{OFF}{track_metadata["title"]}was already downloaded')
|
||||||
return
|
return
|
||||||
|
|
||||||
desc = get_description(track_url_dict, track_metadata, multiple)
|
desc = get_description(track_url_dict, track_metadata, multiple)
|
||||||
@ -159,8 +162,7 @@ def download_and_tag(
|
|||||||
embed_art,
|
embed_art,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error tagging the file: " + str(e))
|
logger.error(f"{RED}Error tagging the file: {e}")
|
||||||
os.remove(filename)
|
|
||||||
|
|
||||||
|
|
||||||
def download_id_by_type(
|
def download_id_by_type(
|
||||||
@ -194,16 +196,18 @@ def download_id_by_type(
|
|||||||
meta.get("release_type") != "album"
|
meta.get("release_type") != "album"
|
||||||
or meta.get("artist").get("name") == "Various Artists"
|
or meta.get("artist").get("name") == "Various Artists"
|
||||||
):
|
):
|
||||||
print("Ignoring Single/EP/VA: " + meta.get("title", ""))
|
logger.info(f'{OFF}Ignoring Single/EP/VA: {meta.get("title", "")}')
|
||||||
return
|
return
|
||||||
|
|
||||||
album_title = get_title(meta)
|
album_title = get_title(meta)
|
||||||
album_format, quality_met = get_format(client, meta, quality)
|
album_format, quality_met = get_format(client, meta, quality)
|
||||||
if not downgrade_quality and not quality_met:
|
if not downgrade_quality and not quality_met:
|
||||||
print("Skipping release as doesn't met quality requirement")
|
logger.info(
|
||||||
|
f"{OFF}Skipping {album_title} as doesn't met quality requirement"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
print("\nDownloading: {}\n".format(album_title))
|
logger.info(f"\n{YELLOW}Downloading: {album_title} [{album_format}]\n")
|
||||||
dirT = (
|
dirT = (
|
||||||
meta["artist"]["name"],
|
meta["artist"]["name"],
|
||||||
album_title,
|
album_title,
|
||||||
@ -225,7 +229,7 @@ def download_id_by_type(
|
|||||||
try:
|
try:
|
||||||
parse = client.get_track_url(i["id"], quality)
|
parse = client.get_track_url(i["id"], quality)
|
||||||
except requests.exceptions.HTTPError:
|
except requests.exceptions.HTTPError:
|
||||||
print("Nothing found")
|
logger.info(f"{OFF}Nothing found")
|
||||||
continue
|
continue
|
||||||
if "sample" not in parse and parse["sampling_rate"]:
|
if "sample" not in parse and parse["sampling_rate"]:
|
||||||
is_mp3 = True if int(quality) == 5 else False
|
is_mp3 = True if int(quality) == 5 else False
|
||||||
@ -241,22 +245,24 @@ def download_id_by_type(
|
|||||||
i["media_number"] if is_multiple else None,
|
i["media_number"] if is_multiple else None,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print("Demo. Skipping")
|
logger.info(f"{OFF}Demo. Skipping")
|
||||||
count = count + 1
|
count = count + 1
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
parse = client.get_track_url(item_id, quality)
|
parse = client.get_track_url(item_id, quality)
|
||||||
except requests.exceptions.HTTPError:
|
except requests.exceptions.HTTPError:
|
||||||
print("Nothing found")
|
logger.info(f"{OFF}Nothing found")
|
||||||
return
|
return
|
||||||
|
|
||||||
if "sample" not in parse and parse["sampling_rate"]:
|
if "sample" not in parse and parse["sampling_rate"]:
|
||||||
meta = client.get_track_meta(item_id)
|
meta = client.get_track_meta(item_id)
|
||||||
track_title = get_title(meta)
|
track_title = get_title(meta)
|
||||||
print("\nDownloading: {}\n".format(track_title))
|
logger.info(f"\n{YELLOW}Downloading: {track_title}")
|
||||||
track_format, quality_met = get_format(client, meta, quality, True, parse)
|
track_format, quality_met = get_format(client, meta, quality, True, parse)
|
||||||
if not downgrade_quality and not quality_met:
|
if not downgrade_quality and not quality_met:
|
||||||
print("Skipping track as doesn't met quality requirement")
|
logger.info(
|
||||||
|
f"{OFF}Skipping {track_title} as doesn't met quality requirement"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
dirT = (
|
dirT = (
|
||||||
meta["album"]["artist"]["name"],
|
meta["album"]["artist"]["name"],
|
||||||
@ -271,5 +277,5 @@ def download_id_by_type(
|
|||||||
is_mp3 = True if int(quality) == 5 else False
|
is_mp3 = True if int(quality) == 5 else False
|
||||||
download_and_tag(dirn, count, parse, meta, meta, True, is_mp3, embed_art)
|
download_and_tag(dirn, count, parse, meta, meta, True, is_mp3, embed_art)
|
||||||
else:
|
else:
|
||||||
print("Demo. Skipping")
|
logger.info(f"{OFF}Demo. Skipping")
|
||||||
print("\nCompleted\n")
|
logger.info(f"{GREEN}Completed")
|
||||||
|
@ -12,3 +12,7 @@ class InvalidAppIdError(Exception):
|
|||||||
|
|
||||||
class InvalidAppSecretError(Exception):
|
class InvalidAppSecretError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidQuality(Exception):
|
||||||
|
pass
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
from mutagen.flac import FLAC, Picture
|
from mutagen.flac import FLAC, Picture
|
||||||
from mutagen.mp3 import EasyMP3
|
from mutagen.mp3 import EasyMP3
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_title(track_dict):
|
def get_title(track_dict):
|
||||||
try:
|
try:
|
||||||
@ -38,6 +41,7 @@ def tag_flac(filename, root_dir, final_name, d, album, istrack=True, em_image=Fa
|
|||||||
audio["TITLE"] = get_title(d)
|
audio["TITLE"] = get_title(d)
|
||||||
|
|
||||||
audio["TRACKNUMBER"] = str(d["track_number"]) # TRACK NUMBER
|
audio["TRACKNUMBER"] = str(d["track_number"]) # TRACK NUMBER
|
||||||
|
audio["DISCNUMBER"] = str(d["media_number"])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
audio["COMPOSER"] = d["composer"]["name"] # COMPOSER
|
audio["COMPOSER"] = d["composer"]["name"] # COMPOSER
|
||||||
@ -80,7 +84,7 @@ def tag_flac(filename, root_dir, final_name, d, album, istrack=True, em_image=Fa
|
|||||||
image.data = img.read()
|
image.data = img.read()
|
||||||
audio.add_picture(image)
|
audio.add_picture(image)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error embedding image: " + str(e))
|
logger.error(f"Error embedding image: {e}", exc_info=True)
|
||||||
|
|
||||||
audio.save()
|
audio.save()
|
||||||
os.rename(filename, final_name)
|
os.rename(filename, final_name)
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
# original author.
|
# original author.
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@ -12,14 +13,18 @@ from qobuz_dl.exceptions import (
|
|||||||
IneligibleError,
|
IneligibleError,
|
||||||
InvalidAppIdError,
|
InvalidAppIdError,
|
||||||
InvalidAppSecretError,
|
InvalidAppSecretError,
|
||||||
|
InvalidQuality,
|
||||||
)
|
)
|
||||||
|
from qobuz_dl.color import GREEN, YELLOW
|
||||||
|
|
||||||
RESET = "Reset your credentials with 'qobuz-dl -r'"
|
RESET = "Reset your credentials with 'qobuz-dl -r'"
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Client:
|
class Client:
|
||||||
def __init__(self, email, pwd, app_id, secrets):
|
def __init__(self, email, pwd, app_id, secrets):
|
||||||
print("Logging...")
|
logger.info(f"{YELLOW}Logging...")
|
||||||
self.secrets = secrets
|
self.secrets = secrets
|
||||||
self.id = app_id
|
self.id = app_id
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
@ -80,6 +85,8 @@ class Client:
|
|||||||
unix = time.time()
|
unix = time.time()
|
||||||
track_id = kwargs["id"]
|
track_id = kwargs["id"]
|
||||||
fmt_id = kwargs["fmt_id"]
|
fmt_id = kwargs["fmt_id"]
|
||||||
|
if int(fmt_id) not in (5, 6, 7, 27):
|
||||||
|
raise InvalidQuality("Invalid quality id: choose between 5, 6, 7 or 27")
|
||||||
r_sig = "trackgetFileUrlformat_id{}intentstreamtrack_id{}{}{}".format(
|
r_sig = "trackgetFileUrlformat_id{}intentstreamtrack_id{}{}{}".format(
|
||||||
fmt_id, track_id, unix, self.sec
|
fmt_id, track_id, unix, self.sec
|
||||||
)
|
)
|
||||||
@ -94,14 +101,13 @@ class Client:
|
|||||||
else:
|
else:
|
||||||
params = kwargs
|
params = kwargs
|
||||||
r = self.session.get(self.base + epoint, params=params)
|
r = self.session.get(self.base + epoint, params=params)
|
||||||
# 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.\n" + RESET)
|
raise AuthenticationError("Invalid credentials.\n" + RESET)
|
||||||
elif r.status_code == 400:
|
elif r.status_code == 400:
|
||||||
raise InvalidAppIdError("Invalid app id.\n" + RESET)
|
raise InvalidAppIdError("Invalid app id.\n" + RESET)
|
||||||
else:
|
else:
|
||||||
print("Logged: OK")
|
logger.info(f"{GREEN}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.\n" + RESET)
|
raise InvalidAppSecretError("Invalid app secret.\n" + RESET)
|
||||||
@ -115,7 +121,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: {}\n".format(self.label))
|
logger.info(f"{GREEN}Membership: {self.label}")
|
||||||
|
|
||||||
def multi_meta(self, epoint, key, id, type):
|
def multi_meta(self, epoint, key, id, type):
|
||||||
total = 1
|
total = 1
|
||||||
|
@ -4,3 +4,4 @@ mutagen
|
|||||||
tqdm
|
tqdm
|
||||||
pick
|
pick
|
||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
|
colorama
|
||||||
|
2
setup.py
2
setup.py
@ -13,7 +13,7 @@ requirements = read_file("requirements.txt").strip().split()
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name=pkg_name,
|
name=pkg_name,
|
||||||
version="0.6.0",
|
version="0.7.0",
|
||||||
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",
|
||||||
|
Loading…
Reference in New Issue
Block a user