Added support for artists, labels and playlists

This commit is contained in:
vitiko98 2020-11-19 19:53:23 -04:00
parent f6f0645cf7
commit 10b5b256d5
4 changed files with 103 additions and 19 deletions

View File

@ -1,4 +1,4 @@
# Qobuz-DL # qobuz-dl
Seach and download Lossless and Hi-Res music from [Qobuz](https://www.qobuz.com/) Seach and download Lossless and Hi-Res music from [Qobuz](https://www.qobuz.com/)
![Demostration](demo.gif) ![Demostration](demo.gif)
@ -7,13 +7,13 @@ Seach and download Lossless and Hi-Res music from [Qobuz](https://www.qobuz.com/
## Features ## Features
* Download FLAC and MP3 files from Qobuz * Download FLAC and MP3 files from Qobuz
* Forget about links: just search and download music directly from your terminal * Search and download music directly from your terminal with interactive mode
* Queue support * Queue support
* If you still want links, `Qobuz-DL` also has an input url mode * Input url mode with download support for albums, tracks, artists, playlists and labels
## Getting started ## Getting started
> Note: `Qobuz-DL` requires Python >3.6 > Note: `qobuz-dl` requires Python >3.6
> Note 2: You'll need an **active subscription** > Note 2: You'll need an **active subscription**
@ -32,7 +32,7 @@ pip3 install -r requirements.txt
email = "your@email.com" email = "your@email.com"
password = "your_password" password = "your_password"
``` ```
#### Run Qobuz-DL #### Run qobuz-dl
##### Linux / MAC OS ##### Linux / MAC OS
``` ```
python3 main.py python3 main.py
@ -48,13 +48,13 @@ usage: python3 main.py [-h] [-a] [-i] [-q int] [-l int] [-d PATH]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-a enable albums-only search -a enable albums-only search
-i Album/track URL run Qobuz-Dl on URL input mode (download by url) -i album/track/artist/label/playlist URL run qobuz-dl on URL input mode (download by url)
-q int quality (5, 6, 7, 27) (default: 6) [320, LOSSLESS, 24B <96KHZ, 24B >96KHZ] -q int quality (5, 6, 7, 27) (default: 6) [320, LOSSLESS, 24B <96KHZ, 24B >96KHZ]
-l int limit of search results by type (default: 10) -l int limit of search results by type (default: 10)
-d PATH custom directory for downloads (default: 'Qobuz Downloads') -d PATH custom directory for downloads (default: 'Qobuz Downloads')
``` ```
## 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.
Also, you are accepting this: https://static.qobuz.com/apps/api/QobuzAPI-TermsofUse.pdf Also, you are accepting this: https://static.qobuz.com/apps/api/QobuzAPI-TermsofUse.pdf

45
main.py
View File

@ -50,9 +50,8 @@ def musicDir(dir):
def get_id(url): def get_id(url):
return re.match( return re.match(
r"https?://(?:w{0,3}|play|open)\.qobuz\.com/(?:(?" r"https?://(?:w{0,3}|play|open)\.qobuz\.com/(?:(?:album|track|artist"
":album|track)/|[a-z]{2}-[a-z]{2}/album/-?\w+(?:-\w+)" "|playlist|label)/|[a-z]{2}-[a-z]{2}/album/-?\w+(?:-\w+)*-?/|user/library/favorites/)(\w+)",
"*-?/|user/library/favorites/)(\w+)",
url, url,
).group(1) ).group(1)
@ -67,11 +66,41 @@ def searchSelected(Qz, path, albums, ids, types, quality):
) )
def fromUrl(Qz, path, link, quality): def fromUrl(Qz, id, path, quality, album=True):
id = get_id(link) downloader.iterateIDs(Qz, id, path, str(quality), album)
downloader.iterateIDs(
Qz, id, path, str(quality), False if "/track/" in link else True
def handle_urls(url, client, path, quality):
possibles = {
"playlist": {"func": client.get_plist_meta, "iterable_key": "tracks"},
"artist": {"func": client.get_artist_meta, "iterable_key": "albums"},
"label": {"func": client.get_label_meta, "iterable_key": "albums"},
"album": {"album": True, "func": None, "iterable_key": None},
"track": {"album": False, "func": None, "iterable_key": None},
}
try:
url_type = url.split("/")[3]
type_dict = possibles[url_type]
item_id = get_id(url)
print("Downloading {}...".format(url_type))
except KeyError:
print("Invalid url. Use urls from https://play.qobuz.com!")
return
if type_dict["func"]:
items = [
item[type_dict["iterable_key"]]["items"]
for item in type_dict["func"](item_id)
][0]
for item in items:
fromUrl(
client,
item["id"],
path,
quality,
True if type_dict["iterable_key"] == "albums" else False,
) )
else:
fromUrl(client, item_id, path, quality, type_dict["album"])
def interactive(Qz, path, limit, tracks=True): def interactive(Qz, path, limit, tracks=True):
@ -120,7 +149,7 @@ def main():
if not arguments.i: if not arguments.i:
interactive(Qz, directory, arguments.l, not arguments.a) interactive(Qz, directory, arguments.l, not arguments.a)
else: else:
fromUrl(Qz, directory, arguments.i, arguments.q) handle_urls(arguments.i, Qz, directory, arguments.q)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -69,7 +69,11 @@ def iterateIDs(client, id, path, quality, album=False):
getCover(meta["image"]["large"], dirn) getCover(meta["image"]["large"], dirn)
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:
url = parse["url"] url = parse["url"]
except KeyError:
print("Track is not available for download")
return
if "sample" not in parse: 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) downloadItem(dirn, count, parse, i, meta, url, False, is_mp3)

View File

@ -8,8 +8,12 @@ import time
import requests import requests
from qo_utils import spoofbuz from qo_utils import spoofbuz
from qo_utils.exceptions import (AuthenticationError, IneligibleError, from qo_utils.exceptions import (
InvalidAppIdError, InvalidAppSecretError) AuthenticationError,
IneligibleError,
InvalidAppIdError,
InvalidAppSecretError,
)
class Client: class Client:
@ -43,6 +47,28 @@ class Client:
params = {"query": kwargs["query"], "limit": kwargs["limit"]} params = {"query": kwargs["query"], "limit": kwargs["limit"]}
elif epoint == "album/search?": elif epoint == "album/search?":
params = {"query": kwargs["query"], "limit": kwargs["limit"]} params = {"query": kwargs["query"], "limit": kwargs["limit"]}
elif epoint == "playlist/get?":
params = {
"extra": "tracks",
"playlist_id": kwargs["id"],
"limit": 500,
"offset": kwargs["offset"],
}
elif epoint == "artist/get?":
params = {
"app_id": self.id,
"artist_id": kwargs["id"],
"limit": 500,
"offset": kwargs["offset"],
"extra": "albums",
}
elif epoint == "label/get?":
params = {
"label_id": kwargs["id"],
"limit": 500,
"offset": kwargs["offset"],
"extra": "albums",
}
elif epoint == "userLibrary/getAlbumsList?": elif epoint == "userLibrary/getAlbumsList?":
unix = time.time() unix = time.time()
r_sig = "userLibrarygetAlbumsList" + str(unix) + kwargs["sec"] r_sig = "userLibrarygetAlbumsList" + str(unix) + kwargs["sec"]
@ -92,6 +118,22 @@ class Client:
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: {}".format(self.label))
def multi_meta(self, epoint, key, id, type):
total = 1
offset = 0
while total > 0:
if type in ["tracks", "albums"]:
j = self.api_call(epoint, id=id, offset=offset, type=type)[type]
else:
j = self.api_call(epoint, id=id, offset=offset, type=type)
if offset == 0:
yield j
total = j[key] - 500
else:
yield j
total -= 500
offset += 500
def get_album_meta(self, id): def get_album_meta(self, id):
return self.api_call("album/get?", id=id) return self.api_call("album/get?", id=id)
@ -101,6 +143,15 @@ class Client:
def get_track_url(self, id, fmt_id): def get_track_url(self, id, fmt_id):
return self.api_call("track/getFileUrl?", id=id, fmt_id=fmt_id) return self.api_call("track/getFileUrl?", id=id, fmt_id=fmt_id)
def get_artist_meta(self, id):
return self.multi_meta("artist/get?", "albums_count", id, None)
def get_plist_meta(self, id):
return self.multi_meta("playlist/get?", "tracks_count", id, None)
def get_label_meta(self, id):
return self.multi_meta("label/get?", "albums_count", id, None)
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)