mirror of
https://github.com/Wojtek242/qobuz-dl.git
synced 2024-11-22 11:05:25 +01:00
Added support for artists, labels and playlists
This commit is contained in:
parent
f6f0645cf7
commit
10b5b256d5
14
README.md
14
README.md
@ -1,4 +1,4 @@
|
||||
# Qobuz-DL
|
||||
# qobuz-dl
|
||||
Seach and download Lossless and Hi-Res music from [Qobuz](https://www.qobuz.com/)
|
||||
|
||||
![Demostration](demo.gif)
|
||||
@ -7,13 +7,13 @@ Seach and download Lossless and Hi-Res music from [Qobuz](https://www.qobuz.com/
|
||||
## Features
|
||||
|
||||
* 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
|
||||
* 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
|
||||
|
||||
> Note: `Qobuz-DL` requires Python >3.6
|
||||
> Note: `qobuz-dl` requires Python >3.6
|
||||
|
||||
> Note 2: You'll need an **active subscription**
|
||||
|
||||
@ -32,7 +32,7 @@ pip3 install -r requirements.txt
|
||||
email = "your@email.com"
|
||||
password = "your_password"
|
||||
```
|
||||
#### Run Qobuz-DL
|
||||
#### Run qobuz-dl
|
||||
##### Linux / MAC OS
|
||||
```
|
||||
python3 main.py
|
||||
@ -48,13 +48,13 @@ usage: python3 main.py [-h] [-a] [-i] [-q int] [-l int] [-d PATH]
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-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]
|
||||
-l int limit of search results by type (default: 10)
|
||||
-d PATH custom directory for downloads (default: 'Qobuz Downloads')
|
||||
```
|
||||
## 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
|
||||
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
|
||||
|
47
main.py
47
main.py
@ -50,9 +50,8 @@ def musicDir(dir):
|
||||
|
||||
def get_id(url):
|
||||
return re.match(
|
||||
r"https?://(?:w{0,3}|play|open)\.qobuz\.com/(?:(?"
|
||||
":album|track)/|[a-z]{2}-[a-z]{2}/album/-?\w+(?:-\w+)"
|
||||
"*-?/|user/library/favorites/)(\w+)",
|
||||
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)
|
||||
|
||||
@ -67,11 +66,41 @@ def searchSelected(Qz, path, albums, ids, types, quality):
|
||||
)
|
||||
|
||||
|
||||
def fromUrl(Qz, path, link, quality):
|
||||
id = get_id(link)
|
||||
downloader.iterateIDs(
|
||||
Qz, id, path, str(quality), False if "/track/" in link else True
|
||||
)
|
||||
def fromUrl(Qz, id, path, quality, album=True):
|
||||
downloader.iterateIDs(Qz, id, path, str(quality), album)
|
||||
|
||||
|
||||
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):
|
||||
@ -120,7 +149,7 @@ def main():
|
||||
if not arguments.i:
|
||||
interactive(Qz, directory, arguments.l, not arguments.a)
|
||||
else:
|
||||
fromUrl(Qz, directory, arguments.i, arguments.q)
|
||||
handle_urls(arguments.i, Qz, directory, arguments.q)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -69,7 +69,11 @@ def iterateIDs(client, id, path, quality, album=False):
|
||||
getCover(meta["image"]["large"], dirn)
|
||||
for i in meta["tracks"]["items"]:
|
||||
parse = client.get_track_url(i["id"], quality)
|
||||
url = parse["url"]
|
||||
try:
|
||||
url = parse["url"]
|
||||
except KeyError:
|
||||
print("Track is not available for download")
|
||||
return
|
||||
if "sample" not in parse:
|
||||
is_mp3 = True if int(quality) == 5 else False
|
||||
downloadItem(dirn, count, parse, i, meta, url, False, is_mp3)
|
||||
|
@ -8,8 +8,12 @@ import time
|
||||
import requests
|
||||
|
||||
from qo_utils import spoofbuz
|
||||
from qo_utils.exceptions import (AuthenticationError, IneligibleError,
|
||||
InvalidAppIdError, InvalidAppSecretError)
|
||||
from qo_utils.exceptions import (
|
||||
AuthenticationError,
|
||||
IneligibleError,
|
||||
InvalidAppIdError,
|
||||
InvalidAppSecretError,
|
||||
)
|
||||
|
||||
|
||||
class Client:
|
||||
@ -43,6 +47,28 @@ class Client:
|
||||
params = {"query": kwargs["query"], "limit": kwargs["limit"]}
|
||||
elif epoint == "album/search?":
|
||||
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?":
|
||||
unix = time.time()
|
||||
r_sig = "userLibrarygetAlbumsList" + str(unix) + kwargs["sec"]
|
||||
@ -92,6 +118,22 @@ class Client:
|
||||
self.label = usr_info["user"]["credential"]["parameters"]["short_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):
|
||||
return self.api_call("album/get?", id=id)
|
||||
|
||||
@ -101,6 +143,15 @@ class Client:
|
||||
def get_track_url(self, 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):
|
||||
return self.api_call("album/search?", query=query, limit=limit)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user