Compare commits

...

3 Commits

Author SHA1 Message Date
c-basalt 654173ac66 minor fixes 2024-04-23 17:02:43 -04:00
c-basalt 55ee6f8dfe update testcase 2024-04-23 16:51:33 -04:00
c-basalt cf6851d916 change api use 2024-04-23 16:34:19 -04:00
1 changed files with 64 additions and 109 deletions

View File

@ -1,8 +1,6 @@
import base64
import json
import hashlib
import random
import re
import time
from .common import InfoExtractor
@ -10,6 +8,7 @@
clean_html,
int_or_none,
join_nonempty,
js_to_json,
strip_jsonp,
str_or_none,
traverse_obj,
@ -20,33 +19,6 @@
class QQMusicBaseIE(InfoExtractor):
def _get_sign(self, payload: bytes):
# This may not work for domains other than `y.qq.com` and browswer UA that contains `Headless`
md5hex = hashlib.md5(payload).hexdigest().upper()
hex_digits = [int(c, base=16) for c in md5hex]
xor_digits = []
for i, xor in enumerate([212, 45, 80, 68, 195, 163, 163, 203, 157, 220, 254, 91, 204, 79, 104, 6]):
xor_digits.append((hex_digits[i * 2] * 16 + hex_digits[i * 2 + 1]) ^ xor)
char_indicies = []
for i in range(0, len(xor_digits) - 1, 3):
char_indicies.extend([
xor_digits[i] >> 2,
((xor_digits[i] & 3) << 4) | (xor_digits[i + 1] >> 4),
((xor_digits[i + 1] & 15) << 2) | (xor_digits[i + 2] >> 6),
xor_digits[i + 2] & 63,
])
char_indicies.extend([
xor_digits[15] >> 2,
(xor_digits[15] & 3) << 4,
])
head = ''.join(md5hex[i] for i in [21, 4, 9, 26, 16, 20, 27, 30])
tail = ''.join(md5hex[i] for i in [18, 11, 3, 2, 1, 7, 6, 25])
body = ''.join('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='[i] for i in char_indicies)
return re.sub(r'[\/+]', '', f'zzb{head}{body}{tail}'.lower())
def _get_g_tk(self):
n = 5381
for chr in self._get_cookies('https://y.qq.com').get('qqmusic_key', ''):
@ -63,33 +35,24 @@ def m_r_get_ruin():
curMs = int(time.time() * 1000) % 1000
return int(round(random.random() * 2147483647) * curMs % 1E10)
def download_init_data(self, url, mid):
webpage = self._download_webpage(url, mid)
return self._search_json(
r'window\.__INITIAL_DATA__\s*=\s*',
webpage.replace('undefined', 'null'), 'init data', mid)
def download_init_data(self, url, mid, fatal=True):
webpage = self._download_webpage(url, mid, fatal=fatal)
return self._search_json(r'window\.__INITIAL_DATA__\s*=\s*', webpage,
'init data', mid, transform_source=js_to_json, fatal=fatal)
def make_fcu_req(self, mid, req_dict, **kwargs):
def make_fcu_req(self, req_dict, mid, **kwargs):
payload = json.dumps({
'comm': {
'cv': 4747474,
'cv': 0,
'ct': 24,
'format': 'json',
'inCharset': 'utf-8',
'outCharset': 'utf-8',
'notice': 0,
'platform': 'yqq.json',
'needNewCode': 1,
'uin': self._get_uin(),
'g_tk_new_20200303': self._get_g_tk(),
'g_tk': self._get_g_tk(),
},
**req_dict
}, separators=(',', ':')).encode('utf-8')
return self._download_json(
'https://u.y.qq.com/cgi-bin/musics.fcg', mid, data=payload,
query={'_': int(time.time()), 'sign': self._get_sign(payload)}, **kwargs)
return self._download_json('https://u.y.qq.com/cgi-bin/musicu.fcg',
mid, data=payload, **kwargs)
class QQMusicIE(QQMusicBaseIE):
@ -103,10 +66,11 @@ class QQMusicIE(QQMusicBaseIE):
'id': '004Ti8rT003TaZ',
'ext': 'mp3',
'title': '永夜のパレード (永夜的游行)',
'album': '幻想遊園郷 -Fantastic Park-',
'release_date': '20111230',
'duration': 281,
'creators': ['ケーキ姫', 'JUMA'],
'album': '幻想遊園郷 -Fantastic Park-',
'genres': ['Pop'],
'description': 'md5:b5261f3d595657ae561e9e6aee7eb7d9',
'size': 4501244,
'thumbnail': r're:^https?://.*\.jpg(?:$|[#?])',
@ -124,6 +88,7 @@ class QQMusicIE(QQMusicBaseIE):
'release_date': '20150129',
'duration': 298,
'creators': ['林俊杰'],
'genres': ['Pop'],
'description': 'md5:f568421ff618d2066e74b65a04149c4e',
'thumbnail': r're:^https?://.*\.jpg(?:$|[#?])',
},
@ -172,63 +137,71 @@ class QQMusicIE(QQMusicBaseIE):
def _real_extract(self, url):
mid = self._match_id(url)
init_data = self.download_init_data(url, mid)
media_id = traverse_obj(init_data, (
'songList', lambda _, v: v['mid'] == mid, 'file', 'media_mid'), get_all=False)
init_data = self.download_init_data(url, mid, fatal=False)
info_data = self.make_fcu_req({'info': {
'module': 'music.pf_song_detail_svr',
'method': 'get_song_detail_yqq',
'param': {
'song_mid': mid,
'song_type': 0,
}
}}, mid, note='Downloading song info')['info']['data']['track_info']
data = self.make_fcu_req(mid, {
media_mid = info_data['file']['media_mid']
data = self.make_fcu_req({
'req_1': {
'module': 'vkey.GetVkeyServer',
'method': 'CgiGetVkey',
'param': {
'guid': str(self.m_r_get_ruin()),
'songmid': [mid],
'songtype': [0],
'uin': self._get_cookies('https://y.qq.com').get('o_cookie', '0'),
'songmid': [mid] * len(self._FORMATS),
'songtype': [0] * len(self._FORMATS),
'uin': str(self._get_uin()),
'loginflag': 1,
'platform': '20',
**({
'songmid': [mid] * len(self._FORMATS),
'songtype': [0] * len(self._FORMATS),
'filename': [f'{f["prefix"]}{media_id}.{f["ext"]}' for f in self._FORMATS.values()],
} if media_id else {}),
'filename': [f'{f["prefix"]}{media_mid}.{f["ext"]}' for f in self._FORMATS.values()],
}
},
'req_2': {
'module': 'music.musichallSong.PlayLyricInfo',
'method': 'GetPlayLyricInfo',
'param': {
'songMID': mid,
'songID': traverse_obj(init_data, ('detail', 'id', {int})),
}
}})
'param': {'songMID': mid},
},
}, mid, note='Downloading formats and lyric')
if data['req_1']['code'] != 0:
raise ExtractorError(f'Failed to download formats, error {data["req_1"]["code"]}')
formats = traverse_obj(data, ('req_1', 'data', 'midurlinfo', lambda _, v: v['songmid'] == mid and v['purl'], {
'url': ('purl', {str}, {lambda x: f'https://dl.stream.qqmusic.qq.com/{x}'}),
'format': ('filename', {lambda x: self._FORMATS[x[:4]]['name']}),
'format_id': ('filename', {lambda x: self._FORMATS[x[:4]]['name']}),
'size': ('filename', {lambda x: self._FORMATS[x[:4]]['name']},
{lambda x: traverse_obj(init_data, ('songList', ..., 'file', f'size_{x}'), get_all=False)}),
{lambda x: traverse_obj(info_data, ('file', f'size_{x}'), get_all=False)}),
'quality': ('filename', {lambda x: self._FORMATS[x[:4]]['preference']}),
'abr': ('filename', {lambda x: self._FORMATS[x[:4]]['abr']}),
}))
if traverse_obj(data, ('req_2', 'code')):
self.report_warning(f'Failed to download lyric, error {data["req_2"]["code"]}')
lrc_content = traverse_obj(data, ('req_2', 'data', 'lyric', {lambda x: base64.b64decode(x).decode('utf-8')}))
info_dict = {
'id': mid,
'formats': formats,
**traverse_obj(init_data, ('detail', {
**traverse_obj(info_data, {
'title': ('title', {str}),
'album': ('albumName', {str}, {lambda x: x or None}),
'thumbnail': ('picurl', {url_or_none}),
'release_date': ('ctime', {lambda x: x.replace('-', '') or None}),
'description': ('info', 'intro', 'content', ..., 'value', {str}),
}), get_all=False),
**traverse_obj(init_data, ('songList', lambda _, v: v['mid'] == mid, {
'album': ('album', 'title', {str}, {lambda x: x or None}),
'release_date': ('time_public', {lambda x: x.replace('-', '') or None}),
'creators': ('singer', ..., 'name', {str}),
'alt_title': ('subtitle', {str}, {lambda x: x or None}),
'duration': ('interval', {int}),
'duration': ('interval', {int_or_none}),
}),
**traverse_obj(init_data, ('detail', {
'thumbnail': ('picurl', {url_or_none}),
'description': ('info', 'intro', 'content', ..., 'value', {str}),
'genres': ('info', 'genre', 'content', ..., 'value', {str}, {lambda x: [x]}),
}), get_all=False),
'creators': traverse_obj(init_data, ('detail', 'singer', ..., 'name')),
}
if lrc_content:
info_dict['subtitles'] = {'origin': [{'ext': 'lrc', 'data': lrc_content}]}
@ -248,7 +221,7 @@ class QQMusicSingerIE(QQMusicBaseIE):
'description': 'md5:10624ce73b06fa400bc846f59b0305fa',
'thumbnail': r're:^https?://.*\.jpg(?:$|[#?])',
},
'playlist_mincount': 10,
'playlist_mincount': 100,
}, {
'url': 'https://y.qq.com/n/ryqq/singer/000Q00f213YzNV',
'info_dict': {
@ -263,10 +236,11 @@ class QQMusicSingerIE(QQMusicBaseIE):
'id': '0016cvsy02mmCl',
'ext': 'mp3',
'title': '群青',
'album': '桃几2021年翻唱集',
'release_date': '20210913',
'duration': 248,
'creators': ['桃几OvO'],
'album': '桃几2021年翻唱集',
'genres': ['Pop'],
'description': 'md5:4296005a04edcb5cdbe0889d5055a7ae',
'size': 3970822,
'thumbnail': r're:^https?://.*\.jpg(?:$|[#?])',
@ -275,18 +249,18 @@ class QQMusicSingerIE(QQMusicBaseIE):
}]
def _entries(self, mid, init_data):
size = 50
page_size = 50
max_num = traverse_obj(init_data, ('singerDetail', 'songTotalNum'))
for page in range(0, max_num // size + 1):
data = self.make_fcu_req(mid, {'req_1': {
for page in range(0, max_num // page_size + 1):
data = self.make_fcu_req({'req_1': {
'module': 'music.web_singer_info_svr',
'method': 'get_singer_detail_info',
'param': {
'sort': 5,
'singermid': mid,
'sin': page * size,
'num': size,
}}}, note=f'Downloading page {page}')
'sin': page * page_size,
'num': page_size,
}}}, mid, note=f'Downloading page {page}')
yield from traverse_obj(data, ('req_1', 'data', 'songlist', ..., {lambda x: self.url_result(
f'https://y.qq.com/n/ryqq/songDetail/{x["mid"]}', QQMusicIE, x['mid'], x.get('title'))}))
@ -448,7 +422,7 @@ class QQMusicVideoIE(QQMusicBaseIE):
'release_date': '20230709',
'duration': 313,
'creators': ['Duke Dumont'],
'view_count': 542,
'view_count': int,
},
}]
@ -463,15 +437,7 @@ def _parse_url_formats(self, url_data):
def _real_extract(self, url):
video_id = self._match_id(url)
payload = json.dumps({
'comm': {
'ct': 6,
'cv': 0,
'g_tk': self._get_g_tk(),
'uin': self._get_uin(),
'format': 'json',
'platform': 'yqq',
},
video_info = self.make_fcu_req({
'mvInfo': {
'module': 'music.video.VideoData',
'method': 'get_video_info_batch',
@ -479,27 +445,16 @@ def _real_extract(self, url):
'vidlist': [video_id],
'required': [
'vid', 'type', 'sid', 'cover_pic', 'duration', 'singers',
'new_switch_str', 'video_pay', 'hint', 'code', 'msg',
'name', 'desc', 'playcnt', 'pubdate', 'isfav', 'fileid',
'filesize_v2', 'switch_pay_type', 'pay', 'pay_info',
'uploader_headurl', 'uploader_nick', 'uploader_uin',
'uploader_encuin', 'play_forbid_reason']
'video_pay', 'hint', 'code', 'msg', 'name', 'desc',
'playcnt', 'pubdate', 'play_forbid_reason']
}
},
'mvUrl': {
'module': 'music.stream.MvUrlProxy',
'method': 'GetMvUrls',
'param': {
'vids': [video_id],
'request_type': 10003,
'addrtype': 3,
'format': 264,
'maxFiletype': 60,
}
'param': {'vids': [video_id]}
}
}, separators=(',', ':')).encode('utf-8')
video_info = self._download_json('https://u.y.qq.com/cgi-bin/musicu.fcg', video_id, data=payload)
}, video_id)
if traverse_obj(video_info, ('mvInfo', 'data', video_id, 'play_forbid_reason')) == 3:
self.raise_geo_restricted()
@ -507,8 +462,8 @@ def _real_extract(self, url):
'id': video_id,
'formats': self._parse_url_formats(traverse_obj(video_info, ('mvUrl', 'data', video_id))),
**traverse_obj(video_info, ('mvInfo', 'data', video_id, {
'title': ('name', {str_or_none}),
'description': ('desc', {str_or_none}),
'title': ('name', {str}),
'description': ('desc', {str}),
'thumbnail': ('cover_pic', {url_or_none}),
'release_timestamp': ('pubdate', {int_or_none}),
'duration': ('duration', {int_or_none}),