Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a new "me" command to download by liked albums/tracks #185

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# qobuz-dl

Search, explore and download Lossless and Hi-Res music from [Qobuz](https://www.qobuz.com/).
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=VZWSWVGZGJRMU&source=url)

Expand All @@ -8,6 +9,7 @@ Search, explore and download Lossless and Hi-Res music from [Qobuz](https://www.
* Explore and download music directly from your terminal with **interactive** or **lucky** mode
* Download albums, tracks, artists, playlists and labels with **download** mode
* Download music from last.fm playlists (Spotify, Apple Music and Youtube playlists are also supported through this method)
* Download all liked albums or tracks
* Queue support on **interactive** mode
* Effective duplicate handling with own portable database
* Support for albums with multiple discs
Expand Down Expand Up @@ -119,6 +121,22 @@ qobuz-dl lucky jay z story of oj --type track --no-cover

Run `qobuz-dl lucky --help` for more info.

### Me mode

Download all liked albums

```
qobuz-dl me
```

Download all liked tracks

```
qobuz-dl me -t tracks
```

Run `qobuz-dl me --help` for more info.

### Other
Reset your config file
```
Expand Down Expand Up @@ -149,7 +167,7 @@ commands:
lucky lucky mode
```

## Module usage
## Module usage
Using `qobuz-dl` as a module is really easy. Basically, the only thing you need is `QobuzDL` from `core`.

```python
Expand All @@ -172,6 +190,7 @@ Attributes, methods and parameters have been named as self-explanatory as possib

## A note about Qo-DL
`qobuz-dl` is inspired in the discontinued Qo-DL-Reborn. This tool 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. By using it, you are accepting the [Qobuz API Terms of Use](https://static.qobuz.com/apps/api/QobuzAPI-TermsofUse.pdf).
* `qobuz-dl` is not affiliated with Qobuz
- 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).
- `qobuz-dl` is not affiliated with Qobuz
14 changes: 9 additions & 5 deletions qobuz_dl/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,19 @@ def _reset_config(config_file):
config = configparser.ConfigParser()
config["DEFAULT"]["email"] = input("Enter your email:\n- ")
password = input("Enter your password\n- ")
config["DEFAULT"]["password"] = hashlib.md5(password.encode("utf-8")).hexdigest()
config["DEFAULT"]["password"] = hashlib.md5(
password.encode("utf-8")).hexdigest()
config["DEFAULT"]["default_folder"] = (
input("Folder for downloads (leave empty for default 'Qobuz Downloads')\n- ")
or "Qobuz Downloads"
input("Folder for downloads (leave empty for default 'Qobuz Downloads')\n- ") or
"Qobuz Downloads"
)
config["DEFAULT"]["default_quality"] = (
input(
"Download quality (5, 6, 7, 27) "
"[320, LOSSLESS, 24B <96KHZ, 24B >96KHZ]"
"\n(leave empty for default '6')\n- "
)
or "6"
) or
"6"
)
config["DEFAULT"]["default_limit"] = "20"
config["DEFAULT"]["no_m3u"] = "false"
Expand Down Expand Up @@ -86,6 +87,9 @@ def _handle_commands(qobuz, arguments):
qobuz.lucky_type = arguments.type
qobuz.lucky_limit = arguments.number
qobuz.lucky_mode(query)
elif arguments.command == "me":
qobuz.me_type = arguments.type
qobuz.me_mode()
else:
qobuz.interactive_limit = arguments.limit
qobuz.interactive()
Expand Down
18 changes: 17 additions & 1 deletion qobuz_dl/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@ def dl_args(subparsers):
return download


def me_args(subparsers):
me = subparsers.add_parser(
"me",
description="Download by liked albums or tracks.",
help="input mode",
)
me.add_argument(
"-t",
"--type",
default="albums",
help=("type of liked items to search (albums, tracks) (default: albums)"),
)
return me


def add_common_arg(custom_parser, default_folder, default_quality):
custom_parser.add_argument(
"-d",
Expand Down Expand Up @@ -159,9 +174,10 @@ def qobuz_dl_args(
interactive = fun_args(subparsers, default_limit)
download = dl_args(subparsers)
lucky = lucky_args(subparsers)
me = me_args(subparsers)
[
add_common_arg(i, default_folder, default_quality)
for i in (interactive, download, lucky)
for i in (interactive, download, lucky, me)
]

return parser
56 changes: 51 additions & 5 deletions qobuz_dl/core.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import logging
import os
import sys

import requests
from bs4 import BeautifulSoup as bso
from pathvalidate import sanitize_filename
Expand Down Expand Up @@ -41,6 +40,7 @@ def __init__(
embed_art=False,
lucky_limit=1,
lucky_type="album",
me_type="albums",
interactive_limit=20,
ignore_singles_eps=False,
no_m3u_for_playlists=False,
Expand All @@ -58,6 +58,7 @@ def __init__(
self.embed_art = embed_art
self.lucky_limit = lucky_limit
self.lucky_type = lucky_type
self.me_type = me_type
self.interactive_limit = interactive_limit
self.ignore_singles_eps = ignore_singles_eps
self.no_m3u_for_playlists = no_m3u_for_playlists
Expand All @@ -71,7 +72,8 @@ def __init__(

def initialize_client(self, email, pwd, app_id, secrets):
self.client = qopy.Client(email, pwd, app_id, secrets)
logger.info(f"{YELLOW}Set max quality: {QUALITIES[int(self.quality)]}\n")
logger.info(
f"{YELLOW}Set max quality: {QUALITIES[int(self.quality)]}\n")

def get_tokens(self):
bundle = Bundle()
Expand Down Expand Up @@ -206,13 +208,55 @@ def lucky_mode(self, query, download=True):
f"{YELLOW}qobuz-dl will attempt to download the first "
f"{self.lucky_limit} results."
)
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)

if download:
self.download_list_of_urls(results)

return results

def me_mode(self):
logger.info(f"{YELLOW}Downloading all favorites {self.me_type} ...")
possible_types = ["albums", "tracks"]
limit = 500
if self.me_type not in possible_types:
logger.error(
f"{RED}Error : choose type between albums or tracks (default: album)")
return
offset = 0
user_favorites = self.client.api_call(
"favorite/getUserFavorites", me_type=self.me_type, offset=offset, limit=limit)

if self.me_type == "albums":
urls = []
total = user_favorites["albums"]["total"]
logger.info(f"{YELLOW}{total} albums liked !")
while total > 0:
user_favorites = self.client.api_call(
"favorite/getUserFavorites", me_type=self.me_type, offset=offset, limit=limit)
for album in user_favorites["albums"]["items"]:
urls.append(album["url"])
self.download_list_of_urls(urls)
total -= limit
offset += 1

if self.me_type == "tracks":
ids = []
total = user_favorites["tracks"]["total"]
logger.info(f"{YELLOW}{total} tracks liked !")
while total > 0:
user_favorites = self.client.api_call(
"favorite/getUserFavorites", me_type=self.me_type, offset=offset, limit=limit)
for track in user_favorites["tracks"]["items"]:
ids.append(track["id"])
for i in ids:
tracks_directory = os.path.join(
self.directory, "Favorite Tracks")
self.download_from_id(i, False, tracks_directory)
total -= limit
offset += 1

def search_by_type(self, query, item_type, limit=10, lucky=False):
if len(query) < 3:
logger.info("{RED}Your search query is too short or invalid")
Expand Down Expand Up @@ -266,7 +310,8 @@ def search_by_type(self, query, item_type, limit=10, lucky=False):
)

url = "{}{}/{}".format(WEB_URL, item_type, i.get("id", ""))
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
except (KeyError, IndexError):
logger.info(f"{RED}Invalid type: {item_type}")
Expand Down Expand Up @@ -301,7 +346,8 @@ def get_quality_text(option):
selected_type = pick(item_types, "I'll search for:\n[press Intro]")[0][
:-1
].lower()
logger.info(f"{YELLOW}Ok, we'll search for " f"{selected_type}s{RESET}")
logger.info(
f"{YELLOW}Ok, we'll search for " f"{selected_type}s{RESET}")
final_url_list = []
while True:
query = input(
Expand Down
22 changes: 14 additions & 8 deletions qobuz_dl/qopy.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,14 @@ def api_call(self, epoint, **kwargs):
elif epoint == "favorite/getUserFavorites":
unix = time.time()
# r_sig = "userLibrarygetAlbumsList" + str(unix) + kwargs["sec"]
r_sig = "favoritegetUserFavorites" + str(unix) + kwargs["sec"]
r_sig = "favoritegetUserFavorites" + str(unix)
r_sig_hashed = hashlib.md5(r_sig.encode("utf-8")).hexdigest()
params = {
"app_id": self.id,
"user_auth_token": self.uat,
"type": "albums",
"limit": kwargs["limit"],
"offset": kwargs["offset"],
"type": kwargs["me_type"],
"request_ts": unix,
"request_sig": r_sig_hashed,
}
Expand All @@ -89,7 +91,8 @@ def api_call(self, epoint, **kwargs):
track_id = kwargs["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")
raise InvalidQuality(
"Invalid quality id: choose between 5, 6, 7 or 27")
r_sig = "trackgetFileUrlformat_id{}intentstreamtrack_id{}{}{}".format(
fmt_id, track_id, unix, kwargs.get("sec", self.sec)
)
Expand All @@ -112,18 +115,20 @@ def api_call(self, epoint, **kwargs):
else:
logger.info(f"{GREEN}Logged: OK")
elif (
epoint in ["track/getFileUrl", "favorite/getUserFavorites"]
and r.status_code == 400
epoint in ["track/getFileUrl", "favorite/getUserFavorites"] and
r.status_code == 400
):
raise InvalidAppSecretError(f"Invalid app secret: {r.json()}.\n" + RESET)
raise InvalidAppSecretError(
f"Invalid app secret: {r.json()}.\n" + RESET)

r.raise_for_status()
return r.json()

def auth(self, email, pwd):
usr_info = self.api_call("user/login", email=email, pwd=pwd)
if not usr_info["user"]["credential"]["parameters"]:
raise IneligibleError("Free accounts are not eligible to download tracks.")
raise IneligibleError(
"Free accounts are not eligible to download tracks.")
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"]
Expand Down Expand Up @@ -211,4 +216,5 @@ def cfg_setup(self):
break

if self.sec is None:
raise InvalidAppSecretError("Can't find any valid app secret.\n" + RESET)
raise InvalidAppSecretError(
"Can't find any valid app secret.\n" + RESET)