mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2024-11-18 06:09:17 +00:00
migrate to new url and API
This commit is contained in:
parent
74e80de16e
commit
06de66c9df
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue