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/)
|
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
|
||||||
|
47
main.py
47
main.py
@ -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__":
|
||||||
|
@ -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)
|
||||||
url = parse["url"]
|
try:
|
||||||
|
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)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user