Tidal MQA working

This commit is contained in:
nathom 2021-03-28 20:02:46 -07:00
parent 086262e8b7
commit 66a22bf7c3
5 changed files with 75 additions and 47 deletions

View file

@ -4,14 +4,15 @@ import hashlib
import json import json
import logging import logging
import os import os
import sys # import sys
import time import time
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from pprint import pformat, pprint from pprint import pformat # , pprint
from typing import Generator, Sequence, Tuple, Union from typing import Generator, Sequence, Tuple, Union
import click import click
import requests import requests
from requests.packages import urllib3
import tidalapi import tidalapi
from dogpile.cache import make_region from dogpile.cache import make_region
@ -33,6 +34,9 @@ from .exceptions import (
) )
from .spoofbuz import Spoofer from .spoofbuz import Spoofer
urllib3.disable_warnings()
requests.adapters.DEFAULT_RETRIES = 5
os.makedirs(CACHE_DIR, exist_ok=True) os.makedirs(CACHE_DIR, exist_ok=True)
region = make_region().configure( region = make_region().configure(
"dogpile.cache.dbm", "dogpile.cache.dbm",
@ -427,6 +431,7 @@ class DeezerClient(ClientInterface):
return url return url
'''
class TidalClient(ClientInterface): class TidalClient(ClientInterface):
source = "tidal" source = "tidal"
@ -510,12 +515,15 @@ class TidalClient(ClientInterface):
resp = self.session.request("GET", f"tracks/{track_id}/streamUrl", params) resp = self.session.request("GET", f"tracks/{track_id}/streamUrl", params)
resp.raise_for_status() resp.raise_for_status()
return resp.json() return resp.json()
'''
class TidalMQAClient(ClientInterface): class TidalClient(ClientInterface):
source = "tidal" source = "tidal"
def __init__(self): def __init__(self):
self.logged_in = False
self.device_code = None self.device_code = None
self.user_code = None self.user_code = None
self.verification_url = None self.verification_url = None
@ -538,26 +546,54 @@ class TidalMQAClient(ClientInterface):
if access_token is not None: if access_token is not None:
self.token_expiry = token_expiry self.token_expiry = token_expiry
self.refresh_token = refresh_token self.refresh_token = refresh_token
self.login_by_access_token(access_token, user_id) if self.token_expiry - time.time() < 86400: # 1 day
self._refresh_access_token()
else: else:
self.login_new_user() self._login_by_access_token(access_token, user_id)
else:
self._login_new_user()
self.logged_in = True
click.secho("Logged into Tidal", fg='green')
def get(self, item_id, media_type): def get(self, item_id, media_type):
return self._api_get(item_id, media_type) return self._api_get(item_id, media_type)
def search(self, query, media_type="album"): def search(self, query, media_type="album", limit: int = 100):
raise NotImplementedError params = {
"query": query,
"limit": limit,
}
return self._api_get(f"search/{media_type}s", params=params)
def get_file_url(self, track_id, quality: int = 7):
params = {
"audioquality": TIDAL_Q_IDS[quality],
"playbackmode": "STREAM",
"assetpresentation": "FULL",
}
resp = self._api_request(f"tracks/{track_id}/playbackinfopostpaywall", params)
manifest = json.loads(base64.b64decode(resp["manifest"]).decode("utf-8"))
return {
"url": manifest["urls"][0],
"enc_key": manifest.get("keyId"),
"codec": manifest["codecs"],
}
def _login_new_user(self, launch=True):
login_link = f"https://{self._get_device_code()}"
def login_new_user(self):
login_link = self.get_device_code()
click.secho( click.secho(
f"Go to {login_link} to log into Tidal within 5 minutes.", fg="blue" f"Go to {login_link} to log into Tidal within 5 minutes.", fg="blue"
) )
if launch:
click.launch(login_link)
start = time.time() start = time.time()
elapsed = 0 elapsed = 0
while elapsed < 600: # change later while elapsed < 600: # 5 mins to login
elapsed = time.time() - start elapsed = time.time() - start
status = self.check_auth_status() status = self._check_auth_status()
if status == 2: if status == 2:
# pending # pending
time.sleep(4) time.sleep(4)
@ -571,7 +607,7 @@ class TidalMQAClient(ClientInterface):
else: else:
raise Exception raise Exception
def get_device_code(self): def _get_device_code(self):
data = { data = {
"client_id": TIDAL_CLIENT_INFO["id"], "client_id": TIDAL_CLIENT_INFO["id"],
"scope": "r_usr+w_usr+w_sub", "scope": "r_usr+w_usr+w_sub",
@ -588,7 +624,7 @@ class TidalMQAClient(ClientInterface):
self.auth_interval = resp["interval"] self.auth_interval = resp["interval"]
return resp["verificationUriComplete"] return resp["verificationUriComplete"]
def check_auth_status(self): def _check_auth_status(self):
data = { data = {
"client_id": TIDAL_CLIENT_INFO["id"], "client_id": TIDAL_CLIENT_INFO["id"],
"device_code": self.device_code, "device_code": self.device_code,
@ -616,7 +652,7 @@ class TidalMQAClient(ClientInterface):
self.token_expiry = resp["expires_in"] + time.time() self.token_expiry = resp["expires_in"] + time.time()
return 0 return 0
def verify_access_token(self, token): def _verify_access_token(self, token):
headers = { headers = {
"authorization": f"Bearer {token}", "authorization": f"Bearer {token}",
} }
@ -626,7 +662,7 @@ class TidalMQAClient(ClientInterface):
return True return True
def refresh_access_token(self): def _refresh_access_token(self):
data = { data = {
"client_id": TIDAL_CLIENT_INFO["id"], "client_id": TIDAL_CLIENT_INFO["id"],
"refresh_token": self.refresh_token, "refresh_token": self.refresh_token,
@ -639,7 +675,7 @@ class TidalMQAClient(ClientInterface):
(TIDAL_CLIENT_INFO["id"], TIDAL_CLIENT_INFO["secret"]), (TIDAL_CLIENT_INFO["id"], TIDAL_CLIENT_INFO["secret"]),
) )
if resp.get("status") != 200: if resp.get("status", 200) != 200:
raise Exception("Refresh failed") raise Exception("Refresh failed")
self.user_id = resp["user"]["userId"] self.user_id = resp["user"]["userId"]
@ -647,7 +683,7 @@ class TidalMQAClient(ClientInterface):
self.access_token = resp["access_token"] self.access_token = resp["access_token"]
self.token_expiry = resp["expires_in"] + time.time() self.token_expiry = resp["expires_in"] + time.time()
def login_by_access_token(self, token, user_id=None): def _login_by_access_token(self, token, user_id=None):
headers = {"authorization": f"Bearer {token}"} headers = {"authorization": f"Bearer {token}"}
resp = requests.get("https://api.tidal.com/v1/sessions", headers=headers).json() resp = requests.get("https://api.tidal.com/v1/sessions", headers=headers).json()
if resp.get("status", 200) != 200: if resp.get("status", 200) != 200:
@ -693,17 +729,3 @@ class TidalMQAClient(ClientInterface):
def _api_post(self, url, data, auth=None): def _api_post(self, url, data, auth=None):
r = requests.post(url, data=data, auth=auth, verify=False).json() r = requests.post(url, data=data, auth=auth, verify=False).json()
return r return r
def get_file_url(self, track_id, quality: int = 7):
params = {
"audioquality": TIDAL_Q_IDS[quality],
"playbackmode": "STREAM",
"assetpresentation": "FULL",
}
resp = self._api_request(f"tracks/{track_id}/playbackinfopostpaywall", params)
manifest = json.loads(base64.b64decode(resp["manifest"]).decode("utf-8"))
return {
"url": manifest["urls"][0],
"enc_key": manifest.get("keyId", ""),
"codec": manifest["codecs"],
}

View file

@ -8,7 +8,7 @@ APPNAME = "streamrip"
CACHE_DIR = click.get_app_dir(APPNAME) CACHE_DIR = click.get_app_dir(APPNAME)
CONFIG_DIR = click.get_app_dir(APPNAME) CONFIG_DIR = click.get_app_dir(APPNAME)
CONFIG_PATH = os.path.join(CONFIG_DIR, "configmqa.yaml") CONFIG_PATH = os.path.join(CONFIG_DIR, "config.yaml")
LOG_DIR = click.get_app_dir(APPNAME) LOG_DIR = click.get_app_dir(APPNAME)
DB_PATH = os.path.join(LOG_DIR, "downloads.db") DB_PATH = os.path.join(LOG_DIR, "downloads.db")

View file

@ -63,6 +63,7 @@ class MusicDL(list):
:param source: :param source:
:type source: str :type source: str
""" """
if source == "qobuz":
click.secho(f"Enter {capitalize(source)} email:", fg="green") click.secho(f"Enter {capitalize(source)} email:", fg="green")
self.config.file[source]["email"] = input() self.config.file[source]["email"] = input()
click.secho( click.secho(
@ -75,6 +76,8 @@ class MusicDL(list):
self.config.save() self.config.save()
click.secho(f'Credentials saved to config file at "{self.config._path}"') click.secho(f'Credentials saved to config file at "{self.config._path}"')
else:
raise Exception
def assert_creds(self, source: str): def assert_creds(self, source: str):
assert source in ("qobuz", "tidal", "deezer"), f"Invalid source {source}" assert source in ("qobuz", "tidal", "deezer"), f"Invalid source {source}"
@ -82,7 +85,7 @@ class MusicDL(list):
# no login for deezer # no login for deezer
return return
if ( if source == "qobuz" and (
self.config.file[source]["email"] is None self.config.file[source]["email"] is None
or self.config.file[source]["password"] is None or self.config.file[source]["password"] is None
): ):
@ -185,6 +188,9 @@ class MusicDL(list):
self.config.file["qobuz"]["secrets"], self.config.file["qobuz"]["secrets"],
) = client.get_tokens() ) = client.get_tokens()
self.config.save() self.config.save()
elif client.source == 'tidal':
self.config.file['tidal'] = client.get_tokens()
self.config.save()
def parse_urls(self, url: str) -> Tuple[str, str]: def parse_urls(self, url: str) -> Tuple[str, str]:
"""Returns the type of the url and the id. """Returns the type of the url and the id.

View file

@ -33,11 +33,11 @@ from .exceptions import (
from .metadata import TrackMetadata from .metadata import TrackMetadata
from .utils import ( from .utils import (
clean_format, clean_format,
decrypt_mqa_file,
quality_id, quality_id,
safe_get, safe_get,
tidal_cover_url, tidal_cover_url,
tqdm_download, tqdm_download,
decrypt_mqa_file,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -229,7 +229,7 @@ class Track:
raise InvalidSourceError(self.client.source) raise InvalidSourceError(self.client.source)
if dl_info.get("enc_key"): if dl_info.get("enc_key"):
decrypt_mqa_file(temp_file, self.final_path, dl_info['enc_key']) decrypt_mqa_file(temp_file, self.final_path, dl_info["enc_key"])
else: else:
shutil.move(temp_file, self.final_path) shutil.move(temp_file, self.final_path)

View file

@ -1,5 +1,5 @@
import logging
import base64 import base64
import logging
import logging.handlers as handlers import logging.handlers as handlers
import os import os
from string import Formatter from string import Formatter