64 lines
3.4 KiB
Python
64 lines
3.4 KiB
Python
'''
|
|
Function:
|
|
Implementation of DeezerMusicClient Utils
|
|
Author:
|
|
Zhenchao Jin
|
|
WeChat Official Account (微信公众号):
|
|
Charles的皮卡丘
|
|
'''
|
|
import re
|
|
import base64
|
|
import hashlib
|
|
import binascii
|
|
import functools
|
|
from Cryptodome.Cipher import AES, Blowfish
|
|
|
|
|
|
'''DeezerMusicClientUtils'''
|
|
class DeezerMusicClientUtils():
|
|
BLOWFISH_SECRET = "g4el58wc0zvf9na1"
|
|
MUSIC_QUALITIES = ('FLAC', 'MP3_320', 'MP3_128')
|
|
IS_ENCRYPTED_RPATTERN = re.compile("/m(?:obile|edia)/")
|
|
SHARED_TOKENS = ['ZjI4N2JkNzRjM2Q1NGY5YmJmOTc5OTdjNzhkZWJkMzdiMTU4NjRjZDdhM2MwZjk0MjUxNWNjOWIwNGE1MWM1N2RhYmZiOTQ4YWYyNjM0MDFhOTRkZTUxOGI3MjRlZDdmNDBmMjcyMmNlZGMwMTgxZTEwYmZmNDk5MmVjNzc4NzU3MmU1MDUzZjk0Nzc1NjFiZjhkMjcwNDc0NzRiNzMxMTcxNjUyZWQxNzg0YzlmNTdhMTUxZDMxOTk2NmVjY2Ex']
|
|
token_decrypt_func = lambda t: base64.b64decode(str(t).encode('utf-8')).decode('utf-8')
|
|
'''decryptchunk'''
|
|
@staticmethod
|
|
def decryptchunk(key, data):
|
|
return Blowfish.new(key, Blowfish.MODE_CBC, b"\x00\x01\x02\x03\x04\x05\x06\x07").decrypt(data)
|
|
'''generateblowfishkey'''
|
|
@staticmethod
|
|
def generateblowfishkey(track_id: str) -> bytes:
|
|
md5_hash = hashlib.md5(str(track_id).encode()).hexdigest()
|
|
return "".join(chr(functools.reduce(lambda x, y: x ^ y, map(ord, t))) for t in zip(md5_hash[:16], md5_hash[16:], DeezerMusicClientUtils.BLOWFISH_SECRET)).encode()
|
|
'''getencryptedfileurl'''
|
|
@staticmethod
|
|
def getencryptedfileurl(meta_id: str, track_hash: str, media_version: str, format_number: int = 1):
|
|
url_bytes = b"\xa4".join((track_hash.encode(), str(format_number).encode(), str(meta_id).encode(), str(media_version).encode()))
|
|
info_bytes = bytearray(hashlib.md5(url_bytes).hexdigest().encode())
|
|
info_bytes.extend(b"\xa4"); info_bytes.extend(url_bytes); info_bytes.extend(b"\xa4")
|
|
padding_len = 16 - (len(info_bytes) % 16); info_bytes.extend(b"." * padding_len)
|
|
path = binascii.hexlify(AES.new(b"jo6aey6haid2Teih", AES.MODE_ECB).encrypt(info_bytes)).decode("utf-8")
|
|
return f"https://e-cdns-proxy-{track_hash[0]}.dzcdn.net/mobile/1/{path}"
|
|
'''getcoverurl'''
|
|
@staticmethod
|
|
def getcoverurl(pic_id: str):
|
|
if not pic_id: return None
|
|
return f"https://e-cdns-images.dzcdn.net/images/cover/{pic_id}/1200x1200.jpg"
|
|
'''covert2lrclyrics'''
|
|
@staticmethod
|
|
def covert2lrclyrics(lyrics_node: dict):
|
|
lrc_lines = []; lyrics_node.get("writers") and lrc_lines.append(f"[ar:{lyrics_node['writers']}]")
|
|
if (sync_lines := lyrics_node.get("synchronizedLines")):
|
|
for item in sync_lines: lrc_lines.append(f"{item.get('lrcTimestamp', '')}{item.get('line', '')}") if item.get("lrcTimestamp", "") else (lrc_lines.append(f"[{int(item['milliseconds']) // 60000:02d}:{(int(item['milliseconds']) % 60000) / 1000:05.2f}]{item.get('line', '')}") if "milliseconds" in item else None)
|
|
return "\n".join(lrc_lines)
|
|
else:
|
|
return lyrics_node.get("text")
|
|
'''decryptdownloadedaudiofile'''
|
|
@staticmethod
|
|
def decryptdownloadedaudiofile(src_path: str, dst_path: str, blowfish_key: str):
|
|
encrypt_chunk_size = 3 * 2048
|
|
with open(src_path, "rb") as src, open(dst_path, "wb") as dst:
|
|
while True:
|
|
if not (data := src.read(encrypt_chunk_size)): break
|
|
decrypted_chunk = DeezerMusicClientUtils.decryptchunk(blowfish_key, data[:2048]) + data[2048:] if len(data) >= 2048 else data
|
|
dst.write(decrypted_chunk) |