migrate to new url and API

This commit is contained in:
c-basalt 2024-04-22 06:19:59 -04:00
parent 74e80de16e
commit 06de66c9df

View file

@ -1,3 +1,6 @@
import base64
import json
import hashlib
import random import random
import re import re
import time import time
@ -5,23 +8,66 @@
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import ( from ..utils import (
clean_html, clean_html,
int_or_none,
join_nonempty,
strip_jsonp,
str_or_none,
traverse_obj,
unescapeHTML,
url_or_none,
ExtractorError, ExtractorError,
UserNotLive, UserNotLive,
strip_jsonp,
unescapeHTML,
traverse_obj,
str_or_none,
url_or_none,
int_or_none,
) )
class QQMusicIE(InfoExtractor): 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', ''):
n += (n << 5) + ord(chr)
return n & 2147483647
# Reference: m_r_GetRUin() in top_player.js
# http://imgcache.gtimg.cn/music/portal_v3/y/top_player.js
@staticmethod
def m_r_get_ruin():
curMs = int(time.time() * 1000) % 1000
return int(round(random.random() * 2147483647) * curMs % 1E10)
class QQMusicIE(QQMusicBaseIE):
IE_NAME = 'qqmusic' IE_NAME = 'qqmusic'
IE_DESC = 'QQ音乐' IE_DESC = 'QQ音乐'
_VALID_URL = r'https?://y\.qq\.com/n/yqq/song/(?P<id>[0-9A-Za-z]+)\.html' _VALID_URL = r'https?://y\.qq\.com/n/ryqq/songDetail/(?P<id>[0-9A-Za-z]+)'
_TESTS = [{ _TESTS = [{
'url': 'https://y.qq.com/n/yqq/song/004295Et37taLD.html', 'url': 'https://y.qq.com/n/ryqq/songDetail/004295Et37taLD',
'md5': '5f1e6cea39e182857da7ffc5ef5e6bb8', 'md5': '5f1e6cea39e182857da7ffc5ef5e6bb8',
'info_dict': { 'info_dict': {
'id': '004295Et37taLD', 'id': '004295Et37taLD',
@ -68,86 +114,83 @@ class QQMusicIE(InfoExtractor):
'm4a': {'prefix': 'C200', 'ext': 'm4a', 'preference': 10} 'm4a': {'prefix': 'C200', 'ext': 'm4a', 'preference': 10}
} }
# Reference: m_r_GetRUin() in top_player.js
# http://imgcache.gtimg.cn/music/portal_v3/y/top_player.js
@staticmethod
def m_r_get_ruin():
curMs = int(time.time() * 1000) % 1000
return int(round(random.random() * 2147483647) * curMs % 1E10)
def _real_extract(self, url): def _real_extract(self, url):
mid = self._match_id(url) mid = self._match_id(url)
detail_info_page = self._download_webpage( webpage = self._download_webpage(url, mid)
'http://s.plcloud.music.qq.com/fcgi-bin/fcg_yqq_song_detail_info.fcg?songmid=%s&play=0' % mid, init_data = self._search_json(
mid, note='Download song detail info', r'window\.__INITIAL_DATA__\s*=\s*', webpage.replace('undefined', 'null'),
errnote='Unable to get song detail info', encoding='gbk') 'init data', mid, fatal=False)
song_name = self._html_search_regex( payload = json.dumps({
r"songname:\s*'([^']+)'", detail_info_page, 'song name') "comm": {
"cv": 4747474,
"ct": 24,
"format": "json",
"inCharset": "utf-8",
"outCharset": "utf-8",
"notice": 0,
"platform": "yqq.json",
"needNewCode": 1,
"uin": int_or_none(self._get_cookies('https://y.qq.com').get('o_cookie')) or 0,
"g_tk_new_20200303": self._get_g_tk(),
"g_tk": self._get_g_tk(),
},
"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'),
"loginflag": 1,
"platform": "20"
}
},
"req_2": {
"module": "music.musichallSong.PlayLyricInfo",
"method": "GetPlayLyricInfo",
"param": {
"songMID": mid,
"songID": traverse_obj(init_data, ('detail', 'id', {int})),
}
},
}, separators=(',', ':')).encode('utf-8')
publish_time = self._html_search_regex( data = self._download_json(
r'发行时间:(\d{4}-\d{2}-\d{2})', detail_info_page, 'https://u6.y.qq.com/cgi-bin/musics.fcg', mid, data=payload,
'publish time', default=None) query={'_': int(time.time()), 'sign': self._get_sign(payload)})
if publish_time:
publish_time = publish_time.replace('-', '')
singer = self._html_search_regex( formats = traverse_obj(data, ('req_1', 'data', 'midurlinfo', lambda _, v: v['songmid'] == mid, {
r"singer:\s*'([^']+)", detail_info_page, 'singer', default=None) 'url': ('purl', {str}, {lambda x: f'https://dl.stream.qqmusic.qq.com/{x}'}),
}))
lrc_content = self._html_search_regex( lrc_content = traverse_obj(data, ('req_2', 'data', 'lyric', {lambda x: base64.b64decode(x).decode('utf-8')}))
r'<div class="content" id="lrc_content"[^<>]*>([^<>]+)</div>',
detail_info_page, 'LRC lyrics', default=None)
if lrc_content:
lrc_content = lrc_content.replace('\\n', '\n')
thumbnail_url = None
albummid = self._search_regex(
[r'albummid:\'([0-9a-zA-Z]+)\'', r'"albummid":"([0-9a-zA-Z]+)"'],
detail_info_page, 'album mid', default=None)
if albummid:
thumbnail_url = 'http://i.gtimg.cn/music/photo/mid_album_500/%s/%s/%s.jpg' \
% (albummid[-2:-1], albummid[-1], albummid)
guid = self.m_r_get_ruin()
vkey = self._download_json(
'http://base.music.qq.com/fcgi-bin/fcg_musicexpress.fcg?json=3&guid=%s' % guid,
mid, note='Retrieve vkey', errnote='Unable to get vkey',
transform_source=strip_jsonp)['key']
formats = []
for format_id, details in self._FORMATS.items():
formats.append({
'url': 'http://cc.stream.qqmusic.qq.com/%s%s.%s?vkey=%s&guid=%s&fromtag=0'
% (details['prefix'], mid, details['ext'], vkey, guid),
'format': format_id,
'format_id': format_id,
'quality': details['preference'],
'abr': details.get('abr'),
})
self._check_formats(formats, mid)
actual_lrc_lyrics = ''.join(
line + '\n' for line in re.findall(
r'(?m)^(\[[0-9]{2}:[0-9]{2}(?:\.[0-9]{2,})?\][^\n]*|\[[^\]]*\])', lrc_content))
info_dict = { info_dict = {
'id': mid, 'id': mid,
'formats': formats, 'formats': formats,
'title': song_name, **traverse_obj(init_data, ('detail', {
'release_date': publish_time, 'title': ('title', {str}),
'creator': singer, 'album': ('albumName', {str}, {lambda x: x or None}),
'description': lrc_content, 'thumbnail': ('picurl', {url_or_none}),
'thumbnail': thumbnail_url '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, {
'alt_title': ('subtitle', {str}, {lambda x: x or None}),
'duration': ('interval', {int}),
}), get_all=False),
'creator': ' / '.join(traverse_obj(init_data, ('detail', 'singer', ..., 'name'))) or None,
} }
if actual_lrc_lyrics: if lrc_content:
info_dict['subtitles'] = { info_dict['subtitles'] = {
'origin': [{ 'origin': [{
'ext': 'lrc', 'ext': 'lrc',
'data': actual_lrc_lyrics, 'data': lrc_content,
}] }]
} }
info_dict['description'] = join_nonempty(info_dict.get('description'), lrc_content, delim='\n')
return info_dict return info_dict