Start paid deezer implementation

This commit is contained in:
nathom 2021-07-26 15:45:34 -07:00
parent 37e2a7e8c1
commit 0dbbba8f67
5 changed files with 99 additions and 48 deletions

17
poetry.lock generated
View file

@ -90,6 +90,17 @@ category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
[[package]]
name = "deezer-py"
version = "1.0.4"
description = "A wrapper for all Deezer's APIs"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
requests = "*"
[[package]] [[package]]
name = "docutils" name = "docutils"
version = "0.17.1" version = "0.17.1"
@ -451,7 +462,7 @@ python-versions = "*"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.8" python-versions = "^3.8"
content-hash = "06048e747453dcda8fc0beb92254466e7e21bf6136be73ae25abe9468fd379a0" content-hash = "baac80bc5ff3ccb5a23168ac3303732f79cd16dbafad48a5e216bba531baebd7"
[metadata.files] [metadata.files]
alabaster = [ alabaster = [
@ -490,6 +501,10 @@ decorator = [
{file = "decorator-5.0.9-py3-none-any.whl", hash = "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323"}, {file = "decorator-5.0.9-py3-none-any.whl", hash = "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323"},
{file = "decorator-5.0.9.tar.gz", hash = "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5"}, {file = "decorator-5.0.9.tar.gz", hash = "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5"},
] ]
deezer-py = [
{file = "deezer-py-1.0.4.tar.gz", hash = "sha256:73396d09b5ba1b0e3365b6b68b38dd16af71ccb6b825d328cf6740a0cce7a75c"},
{file = "deezer_py-1.0.4-py3-none-any.whl", hash = "sha256:ca60481b0799f5818976d2af52a69acb15f75b443d0bdc4d5e70e48013d933ce"},
]
docutils = [ docutils = [
{file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"},
{file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"},

View file

@ -33,6 +33,7 @@ simple-term-menu = {version = "^1.2.1", platform = 'linux or darwin'}
pick = {version = "^1.0.0", platform = 'win32 or cygwin'} pick = {version = "^1.0.0", platform = 'win32 or cygwin'}
windows-curses = {version = "^2.2.0", platform = 'win32 or cygwin'} windows-curses = {version = "^2.2.0", platform = 'win32 or cygwin'}
Pillow = "^8.3.0" Pillow = "^8.3.0"
deezer-py = "^1.0.4"
[tool.poetry.urls] [tool.poetry.urls]
"Bug Reports" = "https://github.com/nathom/streamrip/issues" "Bug Reports" = "https://github.com/nathom/streamrip/issues"

View file

@ -1,11 +1,13 @@
"""The clients that interact with the service APIs.""" """The clients that interact with the service APIs."""
import base64 import base64
import binascii
import hashlib import hashlib
import json import json
import logging import logging
import re import re
import time import time
import deezer
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Generator, Sequence, Tuple, Union from typing import Generator, Sequence, Tuple, Union
@ -438,10 +440,11 @@ class DeezerClient(Client):
def __init__(self): def __init__(self):
"""Create a DeezerClient.""" """Create a DeezerClient."""
self.session = gen_threadsafe_session() self.client = deezer.Deezer(accept_language="en-US,en;q=0.5")
# self.session = gen_threadsafe_session()
# no login required # no login required
self.logged_in = True # self.logged_in = True
def search(self, query: str, media_type: str = "album", limit: int = 200) -> dict: def search(self, query: str, media_type: str = "album", limit: int = 200) -> dict:
"""Search API for query. """Search API for query.
@ -467,7 +470,7 @@ class DeezerClient(Client):
:param kwargs: :param kwargs:
""" """
logger.debug("Deezer does not require login call, returning") assert self.client.login_via_arl(kwargs["arl"])
def get(self, meta_id: Union[str, int], media_type: str = "album"): def get(self, meta_id: Union[str, int], media_type: str = "album"):
"""Get metadata. """Get metadata.
@ -477,21 +480,31 @@ class DeezerClient(Client):
:param type_: :param type_:
:type type_: str :type type_: str
""" """
url = f"{DEEZER_BASE}/{media_type}/{meta_id}"
item = self.session.get(url).json()
if media_type in ("album", "playlist"):
tracks = self.session.get(f"{url}/tracks", params={"limit": 1000}).json()
item["tracks"] = tracks["data"]
item["track_total"] = len(tracks["data"])
elif media_type == "artist":
albums = self.session.get(f"{url}/albums").json()
item["albums"] = albums["data"]
logger.debug(item) GET_FUNCTIONS = {
return item "track": self.client.api.get_track,
"album": self.client.api.get_album,
"playlist": self.client.api.get_playlist,
"artist": self.client.api.get_artist_discography,
}
@staticmethod get_item = GET_FUNCTIONS[media_type]
def get_file_url(meta_id: Union[str, int], quality: int = 6): return get_item(meta_id)
# url = f"{DEEZER_BASE}/{media_type}/{meta_id}"
# item = self.session.get(url).json()
# if media_type in ("album", "playlist"):
# tracks = self.session.get(f"{url}/tracks", params={"limit": 1000}).json()
# item["tracks"] = tracks["data"]
# item["track_total"] = len(tracks["data"])
# elif media_type == "artist":
# albums = self.session.get(f"{url}/albums").json()
# item["albums"] = albums["data"]
# logger.debug(item)
# return item
def get_file_url(self, meta_id: Union[str, int], quality: int = 2):
"""Get downloadable url for a track. """Get downloadable url for a track.
:param meta_id: The track ID. :param meta_id: The track ID.
@ -499,10 +512,35 @@ class DeezerClient(Client):
:param quality: :param quality:
:type quality: int :type quality: int
""" """
quality = min(DEEZER_MAX_Q, quality) track_info = self.client.gw.get_track(
url = f"{DEEZER_DL}/{get_quality(quality, 'deezer')}/{DEEZER_BASE}/track/{meta_id}" meta_id,
logger.debug(f"Download url {url}") )
return {"url": url} token = track_info["TRACK_TOKEN"]
url = self.client.get_track_url(token, "FLAC")
if url is None:
md5 = track_info["MD5_ORIGIN"]
media_version = track_info["MEDIA_VERSION"]
format_number = 1
url_bytes = b"\xa4".join(
[
md5.encode(),
str(format_number).encode(),
str(meta_id).encode(),
str(media_version).encode(),
]
)
md5val = hashlib.md5(url_bytes).hexdigest()
step2 = (
md5val.encode()
+ b"\xa4"
+ url_bytes
+ b"\xa4"
+ (b"." * (16 - (len(step2) % 16)))
)
urlPart = _ecbCrypt("jo6aey6haid2Teih", step2)
return urlPart.decode("utf-8")
class TidalClient(Client): class TidalClient(Client):

View file

@ -343,7 +343,8 @@ class Track(Media):
:type path: str :type path: str
""" """
os.makedirs(os.path.dirname(path), exist_ok=True) os.makedirs(os.path.dirname(path), exist_ok=True)
shutil.move(self.path, path) shutil.copy(self.path, path)
os.remove(self.path)
self.path = path self.path = path
def _soundcloud_download(self, dl_info: dict): def _soundcloud_download(self, dl_info: dict):

View file

@ -50,6 +50,27 @@ def safe_get(d: dict, *keys: Hashable, default=None):
return res return res
__QUALITY_MAP: Dict[str, Dict[int, Union[int, str]]] = {
"qobuz": {
1: 5,
2: 6,
3: 7,
4: 27,
},
"deezer": {
0: 9,
1: 3,
2: 1,
},
"tidal": {
0: "LOW", # AAC
1: "HIGH", # AAC
2: "LOSSLESS", # CD Quality
3: "HI_RES", # MQA
},
}
def get_quality(quality_id: int, source: str) -> Union[str, int]: def get_quality(quality_id: int, source: str) -> Union[str, int]:
"""Get the source-specific quality id. """Get the source-specific quality id.
@ -59,33 +80,8 @@ def get_quality(quality_id: int, source: str) -> Union[str, int]:
:type source: str :type source: str
:rtype: Union[str, int] :rtype: Union[str, int]
""" """
q_map: Dict[int, Union[int, str]]
if source == "qobuz":
q_map = {
1: 5,
2: 6,
3: 7,
4: 27,
}
elif source == "tidal":
q_map = {
0: "LOW", # AAC
1: "HIGH", # AAC
2: "LOSSLESS", # CD Quality
3: "HI_RES", # MQA
}
elif source == "deezer":
q_map = {
0: 128,
1: 320,
2: 1411,
}
else:
raise InvalidSourceError(source)
possible_keys = set(q_map.keys()) return __QUALITY_MAP[source][quality_id]
assert quality_id in possible_keys, f"{quality_id} must be in {possible_keys}"
return q_map[quality_id]
def get_quality_id(bit_depth: Optional[int], sampling_rate: Optional[int]): def get_quality_id(bit_depth: Optional[int], sampling_rate: Optional[int]):