Files
musicdl-catalog-sync-suite/catalog-sync/musicdl/modules/utils/kuwoutils.py
T

193 lines
10 KiB
Python

'''
Function:
Implementation of KuwoMusicClient Utils
Author:
Zhenchao Jin
WeChat Official Account (微信公众号):
Charles的皮卡丘
'''
import re
import math
import zlib
import base64
'''settings'''
MASK32 = (1 << 32) - 1
MASK64 = (1 << 64) - 1
'''HelperFunctions'''
class HelperFunctions():
@staticmethod
def u64(x: int) -> int: return x & MASK64
@staticmethod
def u32(x: int) -> int: return x & MASK32
@staticmethod
def rangen(n: int): return range(n)
@staticmethod
def power2(n: int) -> int: return 1 << n
@staticmethod
def longarray(*arr): return list(arr)
'''settings'''
SECRET_KEY_SONG, SECRET_KEY_LYRIC = b"ylzsxkwm", b'yeelion'
ARRAYLS = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]
ARRAYLSMASK = HelperFunctions.longarray(0, 0x100001, 0x300003)
ARRAYE = HelperFunctions.longarray(31, 0, 1, 2, 3, 4, -1, -1, 3, 4, 5, 6, 7, 8, -1, -1, 7, 8, 9, 10, 11, 12, -1, -1, 11, 12, 13, 14, 15, 16, -1, -1, 15, 16, 17, 18, 19, 20, -1, -1, 19, 20, 21, 22, 23, 24, -1, -1, 23, 24, 25, 26, 27, 28, -1, -1, 27, 28, 29, 30, 31, 30, -1, -1)
ARRAYIP1 = HelperFunctions.longarray(39, 7, 47, 15, 55, 23, 63, 31, 38, 6, 46, 14, 54, 22, 62, 30, 37, 5, 45, 13, 53, 21, 61, 29, 36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27, 34, 2, 42, 10, 50, 18, 58, 26, 33, 1, 41, 9, 49, 17, 57, 25, 32, 0, 40, 8, 48, 16, 56, 24)
ARRAYIP2 = HelperFunctions.longarray(57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3, 61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7, 56, 48, 40, 32, 24, 16, 8, 0, 58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6)
ARRAYMASK = [HelperFunctions.power2(n) for n in HelperFunctions.rangen(64)]
ARRAYMASK[-1] = -ARRAYMASK[-1]
ARRAYP = HelperFunctions.longarray(15, 6, 19, 20, 28, 11, 27, 16, 0, 14, 22, 25, 4, 17, 30, 9, 1, 7, 23, 13, 31, 26, 2, 8, 18, 12, 29, 5, 21, 10, 3, 24)
ARRAYPC1 = HelperFunctions.longarray(56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 60, 52, 44, 36, 28, 20, 12, 4, 27, 19, 11, 3)
ARRAYPC2 = HelperFunctions.longarray(13, 16, 10, 23, 0, 4, -1, -1, 2, 27, 14, 5, 20, 9, -1, -1, 22, 18, 11, 3, 25, 7, -1, -1, 15, 6, 26, 19, 12, 1, -1, -1, 40, 51, 30, 36, 46, 54, -1, -1, 29, 39, 50, 44, 32, 47, -1, -1, 43, 48, 38, 55, 33, 52, -1, -1, 45, 41, 49, 35, 28, 31, -1, -1)
MATRIXNSBOX = [
[14,4,3,15,2,13,5,3,13,14,6,9,11,2,0,5,4,1,10,12,15,6,9,10,1,8,12,7,8,11,7,0,0,15,10,5,14,4,9,10,7,8,12,3,13,1,3,6,15,12,6,11,2,9,5,0,4,2,11,14,1,7,8,13],
[15,0,9,5,6,10,12,9,8,7,2,12,3,13,5,2,1,14,7,8,11,4,0,3,14,11,13,6,4,1,10,15,3,13,12,11,15,3,6,0,4,10,1,7,8,4,11,14,13,8,0,6,2,15,9,5,7,1,10,12,14,2,5,9],
[10,13,1,11,6,8,11,5,9,4,12,2,15,3,2,14,0,6,13,1,3,15,4,10,14,9,7,12,5,0,8,7,13,1,2,4,3,6,12,11,0,13,5,14,6,8,15,2,7,10,8,15,4,9,11,5,9,0,14,3,10,7,1,12],
[7,10,1,15,0,12,11,5,14,9,8,3,9,7,4,8,13,6,2,1,6,11,12,2,3,0,5,14,10,13,15,4,13,3,4,9,6,10,1,12,11,0,2,5,0,13,14,2,8,15,7,4,15,1,10,7,5,6,12,11,3,8,9,14],
[2,4,8,15,7,10,13,6,4,1,3,12,11,7,14,0,12,2,5,9,10,13,0,3,1,11,15,5,6,8,9,14,14,11,5,6,4,1,3,10,2,12,15,0,13,2,8,5,11,8,0,15,7,14,9,4,12,7,10,9,1,13,6,3],
[12,9,0,7,9,2,14,1,10,15,3,4,6,12,5,11,1,14,13,0,2,8,7,13,15,5,4,10,8,3,11,6,10,4,6,11,7,9,0,6,4,2,13,1,9,15,3,8,15,3,1,14,12,5,11,0,2,12,14,7,5,10,8,13],
[4,1,3,10,15,12,5,0,2,11,9,6,8,7,6,9,11,4,12,15,0,3,10,5,14,13,7,8,13,14,1,2,13,6,14,9,4,1,2,14,11,13,5,0,1,10,8,3,0,11,3,5,9,4,15,2,7,8,12,15,10,7,6,12],
[13,7,10,0,6,9,5,15,8,4,3,10,11,14,12,5,2,11,9,6,15,12,0,3,4,1,14,13,1,2,7,8,1,2,12,15,10,4,0,3,13,14,6,9,7,8,9,6,15,1,5,12,3,10,14,5,8,7,11,0,4,13,2,11],
]
'''KuwoMusicClientUtils'''
class KuwoMusicClientUtils:
'''bittransform'''
@staticmethod
def bittransform(arr_int, n, l):
l2 = 0
for i in HelperFunctions.rangen(n):
idx = arr_int[i]
if idx < 0: continue
if (l & ARRAYMASK[idx]) == 0: continue
l2 |= ARRAYMASK[i]
return HelperFunctions.u64(l2)
'''des64'''
@staticmethod
def des64(longs, l):
p_r, p_source, out = [0] * 8, [0, 0], KuwoMusicClientUtils.bittransform(ARRAYIP2, 64, l)
p_source[0], p_source[1] = HelperFunctions.u32(out), HelperFunctions.u32((out & 0xFFFFFFFF00000000) >> 32)
for i in HelperFunctions.rangen(16):
s_out, R = 0, KuwoMusicClientUtils.bittransform(ARRAYE, 64, p_source[1])
R ^= longs[i]
for j in HelperFunctions.rangen(8): p_r[j] = (R >> (j * 8)) & 0xFF
for sbi in reversed(HelperFunctions.rangen(8)): s_out = (s_out << 4) | (MATRIXNSBOX[sbi][p_r[sbi]] & 0xF)
R, L = KuwoMusicClientUtils.bittransform(ARRAYP, 32, s_out), p_source[0]
p_source[0] = p_source[1]
p_source[1] = HelperFunctions.u32(L ^ R)
p_source.reverse()
out = ((p_source[1] << 32) & 0xFFFFFFFF00000000) | (p_source[0] & 0xFFFFFFFF)
out = KuwoMusicClientUtils.bittransform(ARRAYIP1, 64, out)
return HelperFunctions.u64(out)
'''subkeys'''
@staticmethod
def subkeys(l, longs, mode):
l2 = KuwoMusicClientUtils.bittransform(ARRAYPC1, 56, l)
for i in HelperFunctions.rangen(16):
r = ARRAYLS[i]
mask = ARRAYLSMASK[r]
not_mask = HelperFunctions.u64(~mask)
part1, part2 = HelperFunctions.u64((l2 & mask) << (28 - r)), (l2 & not_mask) >> r
l2 = HelperFunctions.u64(part1 | part2)
longs[i] = KuwoMusicClientUtils.bittransform(ARRAYPC2, 64, l2)
if mode == 1:
for j in HelperFunctions.rangen(8): longs[j], longs[15 - j] = longs[15 - j], longs[j]
'''crypt'''
@staticmethod
def crypt(msg: bytes, key: bytes, mode: int) -> bytes:
l = 0
for i in HelperFunctions.rangen(8): l |= (key[i] & 0xFF) << (i * 8)
l, j, arr_long1 = HelperFunctions.u64(l), len(msg) // 8, [0] * 16
KuwoMusicClientUtils.subkeys(l, arr_long1, mode)
arr_long2 = [0] * j
for m in HelperFunctions.rangen(j):
v = 0
for n in HelperFunctions.rangen(8): v |= (msg[n + m * 8] & 0xFF) << (n * 8)
arr_long2[m] = HelperFunctions.u64(v)
arr_long3 = [0] * ((1 + 8 * (j + 1)) // 8)
for i1 in HelperFunctions.rangen(j): arr_long3[i1] = KuwoMusicClientUtils.des64(arr_long1, arr_long2[i1])
arr_byte1, l2 = msg[j * 8:], 0
for i1 in HelperFunctions.rangen(len(msg) % 8): l2 |= (arr_byte1[i1] & 0xFF) << (i1 * 8)
l2 = HelperFunctions.u64(l2)
if len(arr_byte1) != 0 or mode == 0: arr_long3[j] = KuwoMusicClientUtils.des64(arr_long1, l2)
out_bytes, i4 = bytearray(8 * len(arr_long3)), 0
for l3 in arr_long3:
for i6 in HelperFunctions.rangen(8): out_bytes[i4] = (l3 >> (i6 * 8)) & 0xFF; i4 += 1
return bytes(out_bytes)
'''encrypt'''
@staticmethod
def encrypt(msg: bytes) -> bytes:
return KuwoMusicClientUtils.crypt(msg, SECRET_KEY_SONG, 0)
'''decrypt'''
@staticmethod
def decrypt(msg: bytes) -> bytes:
return KuwoMusicClientUtils.crypt(msg, SECRET_KEY_SONG, 1)
'''encryptquery'''
@staticmethod
def encryptquery(query: str) -> str:
return base64.b64encode(KuwoMusicClientUtils.encrypt(query.encode("utf-8"))).decode("ascii")
'''xorencrypt'''
@staticmethod
def xorencrypt(data: bytes, key: bytes) -> bytes:
key_len = len(key)
output = bytearray(len(data))
for i in range(len(data)): output[i] = data[i] ^ key[i % key_len]
return bytes(output)
'''buildlyricsparams'''
@staticmethod
def buildlyricsparams(music_id, is_get_lyricx: bool = True):
params_str = f"user=12345,web,web,web&requester=localhost&req=1&rid=MUSIC_{music_id}"
if is_get_lyricx: params_str += '&lrcx=1'
buf_str = params_str.encode('utf-8')
encrypted_bytes = KuwoMusicClientUtils.xorencrypt(buf_str, SECRET_KEY_LYRIC)
final_params = base64.b64encode(encrypted_bytes).decode('utf-8')
return final_params
'''decodelyrics'''
@staticmethod
def decodelyrics(buf: bytes, is_get_lyricx: bool):
if buf[:10] != b'tp=content': return ''
try: split_index = buf.index(b'\r\n\r\n') + 4; compressed_data = buf[split_index:]
except ValueError: return ''
try: lrc_data = zlib.decompress(compressed_data)
except zlib.error: return ''
if not is_get_lyricx: return lrc_data.decode('gb18030', errors='ignore')
base64_str = lrc_data.decode('utf-8')
buf_str = base64.b64decode(base64_str)
decrypted_buffer = KuwoMusicClientUtils.xorencrypt(buf_str, SECRET_KEY_LYRIC)
final_lrc = decrypted_buffer.decode('gb18030', errors='ignore')
return final_lrc
'''formatlyricstime'''
@staticmethod
def formatlyricstime(ms):
if math.isnan(ms) or ms < 0: ms = 0
total_seconds = ms / 1000
minutes = math.floor(total_seconds / 60)
seconds = math.floor(total_seconds % 60)
milliseconds = round((ms % 1000))
return f"[{minutes:02}:{seconds:02}.{milliseconds:03}]"
'''convertrawlrc'''
@staticmethod
def convertrawlrc(raw_lrc: str) -> str:
out, i = [], 0
lines, rx_line, rx_word, rx_zh = re.split(r"\r\n|\r|\n", raw_lrc), re.compile(r"^\[(\d{2}:\d{2}\.\d{3})\](.*)$"), re.compile(r"<(-?\d+),(-?\d+)>([^<]*)"), re.compile(r"[\u4e00-\u9fa5]")
while i < len(lines):
line = lines[i]
m = rx_line.match(line)
if not m: out.append(line); i += 1; continue
ts, payload = m.group(1), m.group(2)
if not payload.replace("<0,0>", "").strip(): i += 1; continue
if payload.startswith("<0,0>") and rx_zh.search(payload): i += 1; continue
words = list(rx_word.finditer(payload))
lyric = "".join(w.group(3) for w in words) if words else payload.replace("<0,0>", "").strip(); trans = ""
if i + 1 < len(lines) and (nm := rx_line.match(lines[i + 1])):
next_payload = nm.group(2)
if next_payload.startswith("<0,0>") and rx_zh.search(next_payload): trans = next_payload.replace("<0,0>", "").strip(); i += 1
out.append(f"[{ts}]{lyric}")
if trans: out.append(f"[{ts}]{trans}")
i += 1
return "\n".join(out)