Compare commits

..

2 Commits

Author SHA1 Message Date
c-basalt
4d082cf173 replace using traversal 2023-09-23 09:13:34 -04:00
c-basalt
a677870074 code cleanup 2023-09-23 08:06:12 -04:00

View File

@ -2,8 +2,6 @@ import itertools
import json import json
import re import re
import time import time
from base64 import b64encode
from binascii import hexlify
from hashlib import md5 from hashlib import md5
from random import randint from random import randint
@ -13,11 +11,8 @@ from ..compat import compat_urllib_parse_urlencode
from ..networking import Request from ..networking import Request
from ..utils import ( from ..utils import (
ExtractorError, ExtractorError,
bytes_to_intlist,
clean_html, clean_html,
float_or_none,
int_or_none, int_or_none,
intlist_to_bytes,
str_or_none, str_or_none,
strftime_or_none, strftime_or_none,
traverse_obj, traverse_obj,
@ -29,52 +24,39 @@ from ..utils import (
class NetEaseMusicBaseIE(InfoExtractor): class NetEaseMusicBaseIE(InfoExtractor):
_FORMATS = ['bMusic', 'mMusic', 'hMusic'] _FORMATS = ['bMusic', 'mMusic', 'hMusic', 'sqMusic', 'hrMusic']
_NETEASE_SALT = '3go8&$8*3*3h0k(2)2' _NETEASE_SALT = '3go8&$8*3*3h0k(2)2'
_API_BASE = 'http://music.163.com/api/' _API_BASE = 'http://music.163.com/api/'
@classmethod
def _encrypt(cls, dfsid):
salt_bytes = bytearray(cls._NETEASE_SALT.encode('utf-8'))
string_bytes = bytearray(str(dfsid).encode('ascii'))
salt_len = len(salt_bytes)
for i in range(len(string_bytes)):
string_bytes[i] = string_bytes[i] ^ salt_bytes[i % salt_len]
m = md5()
m.update(bytes(string_bytes))
result = b64encode(m.digest()).decode('ascii')
return result.replace('/', '_').replace('+', '-')
def _create_eapi_cipher(self, api_path, query, cookies): def _create_eapi_cipher(self, api_path, query, cookies):
KEY = b'e82ckenh8dichen8'
request_text = json.dumps({**query, 'header': cookies}, separators=(',', ':')) request_text = json.dumps({**query, 'header': cookies}, separators=(',', ':'))
message = f'nobody{api_path}use{request_text}md5forencrypt'.encode('latin1') message = f'nobody{api_path}use{request_text}md5forencrypt'.encode('latin1')
msg_digest = md5(message).hexdigest() msg_digest = md5(message).hexdigest()
data = pkcs7_padding(bytes_to_intlist( data = pkcs7_padding(list(str.encode(
f'{api_path}-36cd479b6b5-{request_text}-36cd479b6b5-{msg_digest}')) f'{api_path}-36cd479b6b5-{request_text}-36cd479b6b5-{msg_digest}')))
encrypted = intlist_to_bytes(aes_ecb_encrypt(data, bytes_to_intlist(KEY))) encrypted = bytes(aes_ecb_encrypt(data, list(b'e82ckenh8dichen8')))
return b'params=' + hexlify(encrypted).upper() return f'params={encrypted.hex().upper()}'.encode()
def _download_eapi_json(self, path, song_id, query, headers={}, **kwargs): def _download_eapi_json(self, path, song_id, query, headers={}, **kwargs):
cookies = { cookies = {
'osver': None, 'osver': 'undefined',
'deviceId': None, 'deviceId': 'undefined',
'appver': '8.0.0', 'appver': '8.0.0',
'versioncode': '140', 'versioncode': '140',
'mobilename': None, 'mobilename': 'undefined',
'buildver': '1623435496', 'buildver': '1623435496',
'resolution': '1920x1080', 'resolution': '1920x1080',
'__csrf': '', '__csrf': '',
'os': 'pc', 'os': 'pc',
'channel': None, 'channel': 'undefined',
'requestId': f'{int(time.time() * 1000)}_{randint(0, 1000):04}', 'requestId': f'{int(time.time() * 1000)}_{randint(0, 1000):04}',
} }
headers = { headers = {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
'Referer': 'https://music.163.com', 'Referer': 'https://music.163.com',
'Cookie': '; '.join([f'{k}={v if v is not None else "undefined"}' for [k, v] in cookies.items()]), 'Cookie': '; '.join([f'{k}={v}' for k, v in cookies.items()]),
**headers, **headers,
} }
url = urljoin('https://interface3.music.163.com/', f'/eapi{path}') url = urljoin('https://interface3.music.163.com/', f'/eapi{path}')
@ -98,18 +80,22 @@ class NetEaseMusicBaseIE(InfoExtractor):
bitrate = int_or_none(details.get('bitrate')) or 999000 bitrate = int_or_none(details.get('bitrate')) or 999000
data = self._call_player_api(song_id, bitrate) data = self._call_player_api(song_id, bitrate)
for song in try_get(data, lambda x: x['data'], list) or []: for song in traverse_obj(data, ('data', ...)):
song_url = try_get(song, lambda x: x['url']) song_url = traverse_obj(song, ('url', {url_or_none}))
if not song_url: if not song_url:
continue continue
if self._is_valid_url(song_url, info['id'], 'song'): if self._is_valid_url(song_url, info['id'], 'song'):
formats.append({ formats.append({
'url': song_url, 'url': song_url,
'ext': details.get('extension'),
'abr': float_or_none(song.get('br'), scale=1000),
'format_id': song_format, 'format_id': song_format,
'filesize': int_or_none(song.get('size')), **traverse_obj(song, {
'asr': int_or_none(details.get('sr')), 'ext': ('type', {str}),
'abr': ('br', {lambda i: int_or_none(i, scale=1000)}),
'filesize': ('size', {int_or_none}),
}),
**traverse_obj(details, {
'asr': ('sr', {int_or_none}),
}),
}) })
elif err == 0: elif err == 0:
err = try_get(song, lambda x: x['code'], int) err = try_get(song, lambda x: x['code'], int)
@ -126,10 +112,6 @@ class NetEaseMusicBaseIE(InfoExtractor):
return formats return formats
@classmethod
def convert_milliseconds(cls, ms):
return int(round(ms / 1000.0))
def query_api(self, endpoint, video_id, note): def query_api(self, endpoint, video_id, note):
req = Request('%s%s' % (self._API_BASE, endpoint)) req = Request('%s%s' % (self._API_BASE, endpoint))
req.headers['Referer'] = self._API_BASE req.headers['Referer'] = self._API_BASE
@ -184,6 +166,7 @@ class NetEaseMusicIE(NetEaseMusicBaseIE):
'timestamp': 691516800, 'timestamp': 691516800,
'description': 'md5:1ba2f911a2b0aa398479f595224f2141', 'description': 'md5:1ba2f911a2b0aa398479f595224f2141',
'duration': 268, 'duration': 268,
'alt_title': '伴唱:现代人乐队 合唱:总政歌舞团',
'thumbnail': r're:^http.*\.jpg', 'thumbnail': r're:^http.*\.jpg',
}, },
}, { }, {
@ -255,20 +238,18 @@ class NetEaseMusicIE(NetEaseMusicBaseIE):
song_id, 'Downloading lyrics data') song_id, 'Downloading lyrics data')
lyrics = self._process_lyrics(lyrics_info) lyrics = self._process_lyrics(lyrics_info)
alt_title = None
if info.get('transNames'):
alt_title = '/'.join(info.get('transNames'))
return { return {
'id': song_id, 'id': song_id,
'title': info['name'],
'alt_title': alt_title,
'creator': ' / '.join([artist['name'] for artist in info.get('artists', [])]),
'timestamp': self.convert_milliseconds(info.get('album', {}).get('publishTime')),
'thumbnail': info.get('album', {}).get('picUrl'),
'duration': self.convert_milliseconds(info.get('duration', 0)),
'description': lyrics, 'description': lyrics,
'formats': formats, 'formats': formats,
'alt_title': '/'.join(traverse_obj(info, (('transNames', 'alias'), ...))) or None,
'creator': ' / '.join(traverse_obj(info, ('artists', ..., 'name'))),
**traverse_obj(info, {
'title': ('name', {str}),
'timestamp': ('album', 'publishTime', {lambda i: int_or_none(i, scale=1000)}),
'thumbnail': ('album', 'picUrl', {url_or_none}),
'duration': ('duration', {lambda i: int_or_none(i, scale=1000)}),
}),
} }
@ -350,11 +331,12 @@ class NetEaseMusicSingerIE(NetEaseMusicBaseIE):
'artist/%s?id=%s' % (singer_id, singer_id), 'artist/%s?id=%s' % (singer_id, singer_id),
singer_id, 'Downloading singer data') singer_id, 'Downloading singer data')
name = info['artist']['name'] artist_info = info.get('artist', {})
if info['artist']['trans']: name = artist_info.get('name', '')
if artist_info.get('trans'):
name = '%s - %s' % (name, info['artist']['trans']) name = '%s - %s' % (name, info['artist']['trans'])
if info['artist']['alias']: if artist_info.get('alias'):
name = '%s - %s' % (name, ';'.join(info['artist']['alias'])) name = '%s - %s' % (name, ';'.join(map(str, info['artist']['alias'])))
entries = [ entries = [
self.url_result('http://music.163.com/#/song?id=%s' % song['id'], self.url_result('http://music.163.com/#/song?id=%s' % song['id'],
@ -521,6 +503,10 @@ class NetEaseMusicProgramIE(NetEaseMusicBaseIE):
'id': '10141022', 'id': '10141022',
'title': '滚滚电台的有声节目', 'title': '滚滚电台的有声节目',
'description': 'md5:8d594db46cc3e6509107ede70a4aaa3b', 'description': 'md5:8d594db46cc3e6509107ede70a4aaa3b',
'creator': '滚滚电台ORZ',
'timestamp': 1434450733,
'upload_date': '20150616',
'thumbnail': r're:http.*\.jpg',
}, },
'playlist_count': 4, 'playlist_count': 4,
}, { }, {
@ -549,31 +535,31 @@ class NetEaseMusicProgramIE(NetEaseMusicBaseIE):
'dj/program/detail?id=%s' % program_id, 'dj/program/detail?id=%s' % program_id,
program_id, 'Downloading program info')['program'] program_id, 'Downloading program info')['program']
name = info['name'] metainfo = traverse_obj(info, {
description = info['description'] 'title': ('name', {str}),
'description': ('description', {str}),
'creator': ('dj', 'brand', {str}),
'thumbnail': ('coverUrl', {url_or_none}),
'timestamp': ('createTime', {lambda i: int_or_none(i, scale=1000)}),
})
if not self._yes_playlist(info['songs'] and program_id, info['mainSong']['id']): if not self._yes_playlist(info['songs'] and program_id, info['mainSong']['id']):
formats = self.extract_formats(info['mainSong']) formats = self.extract_formats(info['mainSong'])
return { return {
'id': str(info['mainSong']['id']), 'id': str(info['mainSong']['id']),
'title': name,
'description': description,
'creator': info['dj']['brand'],
'timestamp': self.convert_milliseconds(info['createTime']),
'thumbnail': info['coverUrl'],
'duration': self.convert_milliseconds(info.get('duration', 0)),
'formats': formats, 'formats': formats,
'duration': traverse_obj(info, ('mainSong', 'duration', {lambda i: int_or_none(i, scale=1000)})),
**metainfo,
} }
song_ids = [info['mainSong']['id']] song_ids = traverse_obj(info, ((('mainSong', 'id'), ('songs', ..., 'id')), {int_or_none}))
song_ids.extend([song['id'] for song in info['songs']])
entries = [ entries = [
self.url_result('http://music.163.com/#/song?id=%s' % song_id, self.url_result('http://music.163.com/#/song?id=%s' % song_id,
'NetEaseMusic', song_id) 'NetEaseMusic', song_id)
for song_id in song_ids for song_id in song_ids
] ]
return self.playlist_result(entries, program_id, name, description) return self.playlist_result(entries, program_id, **metainfo)
class NetEaseMusicDjRadioIE(NetEaseMusicBaseIE): class NetEaseMusicDjRadioIE(NetEaseMusicBaseIE):
@ -594,8 +580,7 @@ class NetEaseMusicDjRadioIE(NetEaseMusicBaseIE):
def _real_extract(self, url): def _real_extract(self, url):
dj_id = self._match_id(url) dj_id = self._match_id(url)
name = None metainfo = {}
desc = None
entries = [] entries = []
for offset in itertools.count(start=0, step=self._PAGE_SIZE): for offset in itertools.count(start=0, step=self._PAGE_SIZE):
info = self.query_api( info = self.query_api(
@ -609,13 +594,13 @@ class NetEaseMusicDjRadioIE(NetEaseMusicBaseIE):
'NetEaseMusicProgram', program['id']) 'NetEaseMusicProgram', program['id'])
for program in info['programs'] for program in info['programs']
]) ])
if not metainfo:
if name is None: metainfo = traverse_obj(info, ('programs', 0, 'radio', {
radio = info['programs'][0]['radio'] 'title': ('name', {str}),
name = radio['name'] 'description': ('desc', {str}),
desc = radio['desc'] }))
if not info['more']: if not info['more']:
break break
return self.playlist_result(entries, dj_id, name, desc) return self.playlist_result(entries, dj_id, **metainfo)