Initial import: Music_Server, MusicFree, catalog-sync
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
'''
|
||||
Function:
|
||||
Implementation of KugouMusicClient Utils
|
||||
>>> old api: https://trackercdn.kugou.com/i/?cmd=4&pid=1&forceDown=0&vip=1&hash={file_hash}&key={MD5(file_hash+kgcloud)}
|
||||
>>> webv2 play: https://trackercdnbj.kugou.com/i/v2/?cmd=23&pid=1&behavior=play&hash={file_hash}&key={MD5(file_hash+kgcloudv2)}
|
||||
>>> appv2 play: https://trackercdn.kugou.com/i/v2/?appid=1005&pid=2&cmd=25&behavior=play&hash={file_hash}&key={MD5(file_hash+kgcloudv2)}
|
||||
>>> appv2 download: https://trackercdn.kugou.com/i/v2/?cdnBackup=1&behavior=download&pid=1&cmd=21&appid=1001&hash={file_hash}&key={MD5(file_hash+kgcloudv2)}
|
||||
Author:
|
||||
Zhenchao Jin
|
||||
WeChat Official Account (微信公众号):
|
||||
Charles的皮卡丘
|
||||
'''
|
||||
import json
|
||||
import uuid
|
||||
import time
|
||||
import random
|
||||
import base64
|
||||
import hashlib
|
||||
import requests
|
||||
from Crypto.PublicKey import RSA
|
||||
from .misc import safeextractfromdict
|
||||
from typing import Any, Dict, Optional
|
||||
from Crypto.Cipher import AES, PKCS1_v1_5
|
||||
|
||||
|
||||
'''settings'''
|
||||
IS_LITE = True
|
||||
APPID = 3116 if IS_LITE else 1005
|
||||
CLIENTVER = 11440 if IS_LITE else 20489
|
||||
MUSIC_QUALITIES = ('viper_tape', 'viper_clear', 'viper_atmos', 'flac', 'high', '320', '128')
|
||||
SIGNATURE_WEB_SECRET = "NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt"
|
||||
SIGN_KEY_SECRET = "185672dd44712f60bb1736df5a377e82" if IS_LITE else "57ae12eb6890223e355ccfcb74edf70d"
|
||||
SIGNATURE_ANDROID_SECRET = "LnT6xpN3khm36zse0QzvmgTZ3waWdRSA" if IS_LITE else "OIlwieks28dk2k092lksi2UIkp"
|
||||
PUBLIC_RSA_KEY = """-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDECi0Np2UR87scwrvTr72L6oO01rBbbBPriSDFPxr3Z5syug0O24QyQO8bg27+0+4kBzTBTBOZ/WWU0WryL1JSXRTXLgFVxtzIY41Pe7lPOgsfTCn5kZcvKhYKJesKnnJDNr5/abvTGf+rHG3YRwsCHcQ08/q6ifSioBszvb3QiwIDAQAB
|
||||
-----END PUBLIC KEY-----""" if IS_LITE else """-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDIAG7QOELSYoIJvTFJhMpe1s/gbjDJX51HBNnEl5HXqTW6lQ7LC8jr9fWZTwusknp+sVGzwd40MwP6U5yDE27M/X1+UR4tvOGOqp94TJtQ1EPnWGWXngpeIW5GxoQGao1rmYWAu6oi1z9XkChrsUdC6DJE5E221wf/4WLFxwAtRQIDAQAB
|
||||
-----END PUBLIC KEY-----"""
|
||||
|
||||
|
||||
'''KugouMusicClientUtils'''
|
||||
class KugouMusicClientUtils:
|
||||
'''md5hex'''
|
||||
@staticmethod
|
||||
def md5hex(data: Any) -> str:
|
||||
if isinstance(data, (dict, list)): data = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
|
||||
if isinstance(data, str): data = data.encode("utf-8")
|
||||
return hashlib.md5(data).hexdigest()
|
||||
'''randomstring'''
|
||||
@staticmethod
|
||||
def randomstring(length=16) -> str:
|
||||
chars = "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
return "".join(random.choice(chars) for _ in range(length))
|
||||
'''calculatemid'''
|
||||
@staticmethod
|
||||
def calculatemid(seed: str) -> str:
|
||||
return str(int(hashlib.md5(seed.encode("utf-8")).hexdigest(), 16))
|
||||
'''pad'''
|
||||
@staticmethod
|
||||
def pad(data: bytes, block_size: int = 16) -> bytes:
|
||||
pad_len = block_size - len(data) % block_size
|
||||
return data + bytes([pad_len]) * pad_len
|
||||
'''unpad'''
|
||||
@staticmethod
|
||||
def unpad(data: bytes) -> bytes:
|
||||
pad_len = data[-1]
|
||||
return data[:-pad_len]
|
||||
'''rsaencryptpkcs1'''
|
||||
@staticmethod
|
||||
def rsaencryptpkcs1(data: Any, public_key_pem: str = PUBLIC_RSA_KEY) -> str:
|
||||
if isinstance(data, (dict, list)): data = json.dumps(data, separators=(",", ":"), ensure_ascii=False)
|
||||
if isinstance(data, str): data = data.encode("utf-8")
|
||||
rsa_key = RSA.import_key(public_key_pem)
|
||||
cipher = PKCS1_v1_5.new(rsa_key)
|
||||
enc = cipher.encrypt(data)
|
||||
return enc.hex()
|
||||
'''signatureandroid'''
|
||||
@staticmethod
|
||||
def signatureandroid(params: Dict[str, Any], data: str = "") -> str:
|
||||
params_string = "".join(f"{k}={json.dumps(params[k], separators=(',', ':'), ensure_ascii=False) if isinstance(params[k], (dict, list)) else params[k]}" for k in sorted(params.keys()))
|
||||
return KugouMusicClientUtils.md5hex(f"{SIGNATURE_ANDROID_SECRET}{params_string}{data}{SIGNATURE_ANDROID_SECRET}")
|
||||
'''signatureandroidwithsecret'''
|
||||
@staticmethod
|
||||
def signatureandroidwithsecret(params: Dict[str, Any], data: str, secret: str = "OIlwieks28dk2k092lksi2UIkp") -> str:
|
||||
params_string = "".join(f"{k}={json.dumps(params[k], separators=(',', ':'), ensure_ascii=False) if isinstance(params[k], (dict, list)) else params[k]}" for k in sorted(params.keys()))
|
||||
return KugouMusicClientUtils.md5hex(f"{secret}{params_string}{data}{secret}")
|
||||
'''signatureweb'''
|
||||
@staticmethod
|
||||
def signatureweb(params: Dict[str, Any]) -> str:
|
||||
params_string = "".join(f"{k}={params[k]}" for k in sorted(params.keys()))
|
||||
return KugouMusicClientUtils.md5hex(f"{SIGNATURE_WEB_SECRET}{params_string}{SIGNATURE_WEB_SECRET}")
|
||||
'''signkey'''
|
||||
@staticmethod
|
||||
def signkey(hash_value: str, mid: str, userid: str, appid: str) -> str:
|
||||
return KugouMusicClientUtils.md5hex(f"{hash_value}{SIGN_KEY_SECRET}{appid}{mid}{userid or 0}")
|
||||
'''initdevice'''
|
||||
@staticmethod
|
||||
def initdevice(cookies: dict = None):
|
||||
cookies = cookies or {}
|
||||
guid = str(uuid.uuid4())
|
||||
mid = KugouMusicClientUtils.calculatemid(guid)
|
||||
cookies["KUGOU_API_GUID"] = guid
|
||||
cookies["KUGOU_API_MID"] = mid
|
||||
cookies["KUGOU_API_MAC"] = KugouMusicClientUtils.randomstring(12)
|
||||
cookies["KUGOU_API_DEV"] = KugouMusicClientUtils.randomstring(16)
|
||||
return cookies
|
||||
'''updatecookies'''
|
||||
@staticmethod
|
||||
def updatecookies(resp: requests.Response, cookies: dict):
|
||||
for k, v in resp.cookies.items(): cookies[k] = v
|
||||
return cookies
|
||||
'''sendrequest'''
|
||||
@staticmethod
|
||||
def sendrequest(session: requests.Session, method: str, url: str, params: Optional[Dict[str, Any]] = None, data: Optional[Any] = None, headers: Optional[Dict[str, str]] = None, encrypt_type: str = "android", base_url: str = "https://gateway.kugou.com", encrypt_key: bool = False, not_sign: bool = False, response_type: Optional[str] = None, cookies: Optional[Dict[str, str]] = None, cookies_override: Optional[Dict[str, str]] = None, request_overrides: dict = None):
|
||||
# init
|
||||
clienttime = int(time.time())
|
||||
params, headers, used_cookies, request_overrides = params or {}, headers or {}, dict(cookies), request_overrides or {}
|
||||
if cookies_override: used_cookies.update(cookies_override)
|
||||
token, dfid, userid, mid = used_cookies.get("token", ""), used_cookies.get("dfid", "-"), used_cookies.get("userid", 0), used_cookies.get("KUGOU_API_MID", "-")
|
||||
# construct params
|
||||
default_params = {"dfid": dfid, "mid": mid, "uuid": "-", "appid": APPID, "clientver": CLIENTVER, "clienttime": clienttime}
|
||||
if token: default_params["token"] = token
|
||||
if userid: default_params["userid"] = userid
|
||||
params = {**default_params, **params}
|
||||
# encrypt key
|
||||
if encrypt_key: params["key"] = KugouMusicClientUtils.signkey(params["hash"], params["mid"], params.get("userid"), params["appid"])
|
||||
# signature
|
||||
data_str = json.dumps(data, separators=(",", ":"), ensure_ascii=False) if isinstance(data, (dict, list)) else (data or "")
|
||||
if not_sign:
|
||||
if "signature" in params: params.pop("signature", None)
|
||||
else:
|
||||
if "signature" not in params: params["signature"] = KugouMusicClientUtils.signatureweb(params) if encrypt_type == "web" else KugouMusicClientUtils.signatureandroid(params, data_str)
|
||||
# construct headers
|
||||
base_headers = {"User-Agent": "Android15-1070-11083-46-0-DiscoveryDRADProtocol-wifi", "dfid": dfid, "clienttime": str(params["clienttime"]), "mid": mid, "kg-rc": "1", "kg-thash": "5d816a0", "kg-rec": "1", "kg-rf": "B9EDA08A64250DEFFBCADDEE00F8F25F"}
|
||||
final_headers = {**base_headers, **headers}
|
||||
# send request
|
||||
resp = session.request(method, f"{base_url}{url}", params=params, json=data, headers=final_headers, **request_overrides) if isinstance(data, (dict, list)) else session.request(method, f"{base_url}{url}", params=params, data=data, headers=final_headers, **request_overrides)
|
||||
resp.raise_for_status()
|
||||
KugouMusicClientUtils.updatecookies(resp, cookies)
|
||||
# return
|
||||
if response_type == "arraybuffer": return resp.content
|
||||
try: return resp.json()
|
||||
except Exception: return resp.text
|
||||
'''registerdevice'''
|
||||
@staticmethod
|
||||
def registerdevice(session: requests.Session, cookies: dict, request_overrides: dict = None):
|
||||
# construct
|
||||
data_map = {
|
||||
"availableRamSize": 4983533568, "availableRomSize": 48114719, "availableSDSize": 48114717, "basebandVer": "", "batteryLevel": 100, "batteryStatus": 3, "brand": "Redmi", "buildSerial": "unknown",
|
||||
"device": "marble", "imei": cookies.get("KUGOU_API_GUID"), "imsi": "", "manufacturer": "Xiaomi", "uuid": cookies.get("KUGOU_API_GUID"), "accelerometer": False, "accelerometerValue": "",
|
||||
"gravity": False, "gravityValue": "", "gyroscope": False, "gyroscopeValue": "", "light": False, "lightValue": "", "magnetic": False, "magneticValue": "", "orientation": False, "orientationValue": "",
|
||||
"pressure": False, "pressureValue": "", "step_counter": False, "step_counterValue": "", "temperature": False, "temperatureValue": "",
|
||||
}
|
||||
# aes
|
||||
aes_key = KugouMusicClientUtils.randomstring(6).lower(); encrypt_key = KugouMusicClientUtils.md5hex(aes_key)[:16]; encrypt_iv = KugouMusicClientUtils.md5hex(aes_key)[16: 32]
|
||||
cipher = AES.new(encrypt_key.encode("utf-8"), AES.MODE_CBC, encrypt_iv.encode("utf-8"))
|
||||
raw = json.dumps(data_map, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
||||
enc = cipher.encrypt(KugouMusicClientUtils.pad(raw))
|
||||
aes_body = base64.b64encode(enc).decode("utf-8")
|
||||
p = KugouMusicClientUtils.rsaencryptpkcs1({"aes": aes_key, "uid": cookies.get("userid", 0), "token": cookies.get("token", "")})
|
||||
# send request and return result
|
||||
resp_raw: bytes = KugouMusicClientUtils.sendrequest(session, "POST", "/risk/v2/r_register_dev", params={"part": 1, "platid": 1, "p": p}, data=aes_body, base_url="https://userservice.kugou.com", encrypt_type="android", response_type="arraybuffer", cookies=cookies, request_overrides=request_overrides)
|
||||
try:
|
||||
text: str = resp_raw.decode("utf-8"); result = json.loads(text) if text.startswith("{") else None
|
||||
if result: return result
|
||||
except Exception:
|
||||
pass
|
||||
dec_cipher = AES.new(encrypt_key.encode("utf-8"), AES.MODE_CBC, encrypt_iv.encode("utf-8"))
|
||||
decrypted = KugouMusicClientUtils.unpad(dec_cipher.decrypt(resp_raw)).decode("utf-8")
|
||||
result: dict = json.loads(decrypted)
|
||||
if result.get("status") == 1 and safeextractfromdict(result, ['data', 'dfid'], None): cookies["dfid"] = result["data"]["dfid"]
|
||||
return result
|
||||
'''getsongurl'''
|
||||
@staticmethod
|
||||
def getsongurl(session: requests.Session, hash_value: str, album_id: int = 0, album_audio_id: int = 0, quality: str = "128", free_part: bool = False, cookies: dict = None, request_overrides: dict = None):
|
||||
params = {
|
||||
"album_id": int(album_id), "area_code": 1, "hash": hash_value.lower(), "ssa_flag": "is_fromtrack", "version": 11436, "page_id": 151369488 if not IS_LITE else 967177915,
|
||||
"quality": quality, "album_audio_id": int(album_audio_id), "behavior": "play", "pid": 2 if not IS_LITE else 411, "cmd": 26, "pidversion": 3001, "IsFreePart": 1 if free_part else 0,
|
||||
"ppage_id": "463467626,350369493,788954147" if not IS_LITE else "356753938,823673182,967485191", "cdnBackup": 1, "kcard": 0, "module": "",
|
||||
}
|
||||
return KugouMusicClientUtils.sendrequest(session, "GET", "/v5/url", params=params, headers={"x-router": "trackercdn.kugou.com"}, encrypt_type="android", encrypt_key=True, cookies=cookies, cookies_override={'dfid': KugouMusicClientUtils.randomstring(24)}, request_overrides=request_overrides)
|
||||
Reference in New Issue
Block a user