@ -1,12 +1,25 @@
import itertools
import json
import re
import time
from base64 import b64encode
from binascii import hexlify
from datetime import datetime
from hashlib import md5
from random import randint
from . common import InfoExtractor
from . . compat import compat_str , compat_urllib_parse_urlencode
from . . utils import float_or_none , sanitized_Request
from . . aes import aes_ecb_encrypt , pkcs7_padding
from . . compat import compat_urllib_parse_urlencode
from . . utils import (
ExtractorError ,
bytes_to_intlist ,
error_to_compat_str ,
float_or_none ,
int_or_none ,
intlist_to_bytes ,
sanitized_Request ,
try_get ,
)
class NetEaseMusicBaseIE ( InfoExtractor ) :
@ -17,7 +30,7 @@ class NetEaseMusicBaseIE(InfoExtractor):
@classmethod
def _encrypt ( cls , dfsid ) :
salt_bytes = bytearray ( cls . _NETEASE_SALT . encode ( ' utf-8 ' ) )
string_bytes = bytearray ( compat_ str( dfsid ) . encode ( ' ascii ' ) )
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 ]
@ -26,32 +39,106 @@ class NetEaseMusicBaseIE(InfoExtractor):
result = b64encode ( m . digest ( ) ) . decode ( ' ascii ' )
return result . replace ( ' / ' , ' _ ' ) . replace ( ' + ' , ' - ' )
@classmethod
def make_player_api_request_data_and_headers ( cls , song_id , bitrate ) :
KEY = b ' e82ckenh8dichen8 '
URL = ' /api/song/enhance/player/url '
now = int ( time . time ( ) * 1000 )
rand = randint ( 0 , 1000 )
cookie = {
' osver ' : None ,
' deviceId ' : None ,
' appver ' : ' 8.0.0 ' ,
' versioncode ' : ' 140 ' ,
' mobilename ' : None ,
' buildver ' : ' 1623435496 ' ,
' resolution ' : ' 1920x1080 ' ,
' __csrf ' : ' ' ,
' os ' : ' pc ' ,
' channel ' : None ,
' requestId ' : ' {0} _ {1:04} ' . format ( now , rand ) ,
}
request_text = json . dumps (
{ ' ids ' : ' [ {0} ] ' . format ( song_id ) , ' br ' : bitrate , ' header ' : cookie } ,
separators = ( ' , ' , ' : ' ) )
message = ' nobody {0} use {1} md5forencrypt ' . format (
URL , request_text ) . encode ( ' latin1 ' )
msg_digest = md5 ( message ) . hexdigest ( )
data = ' {0} -36cd479b6b5- {1} -36cd479b6b5- {2} ' . format (
URL , request_text , msg_digest )
data = pkcs7_padding ( bytes_to_intlist ( data ) )
encrypted = intlist_to_bytes ( aes_ecb_encrypt ( data , bytes_to_intlist ( KEY ) ) )
encrypted_params = hexlify ( encrypted ) . decode ( ' ascii ' ) . upper ( )
cookie = ' ; ' . join (
[ ' {0} = {1} ' . format ( k , v if v is not None else ' undefined ' )
for [ k , v ] in cookie . items ( ) ] )
headers = {
' User-Agent ' : self . extractor . get_param ( ' http_headers ' ) [ ' User-Agent ' ] ,
' Content-Type ' : ' application/x-www-form-urlencoded ' ,
' Referer ' : ' https://music.163.com ' ,
' Cookie ' : cookie ,
}
return ( ' params= {0} ' . format ( encrypted_params ) , headers )
def _call_player_api ( self , song_id , bitrate ) :
url = ' https://interface3.music.163.com/eapi/song/enhance/player/url '
data , headers = self . make_player_api_request_data_and_headers ( song_id , bitrate )
try :
msg = ' empty result '
result = self . _download_json (
url , song_id , data = data . encode ( ' ascii ' ) , headers = headers )
if result :
return result
except ExtractorError as e :
if type ( e . cause ) in ( ValueError , TypeError ) :
# JSON load failure
raise
except Exception as e :
msg = error_to_compat_str ( e )
self . report_warning ( ' %s API call ( %s ) failed: %s ' % (
song_id , bitrate , msg ) )
return { }
def extract_formats ( self , info ) :
err = 0
formats = [ ]
song_id = info [ ' id ' ]
for song_format in self . _FORMATS :
details = info . get ( song_format )
if not details :
continue
song_file_path = ' / %s / %s . %s ' % (
self . _encrypt ( details [ ' dfsId ' ] ) , details [ ' dfsId ' ] , details [ ' extension ' ] )
# 203.130.59.9, 124.40.233.182, 115.231.74.139, etc is a reverse proxy-like feature
# from NetEase's CDN provider that can be used if m5.music.126.net does not
# work, especially for users outside of Mainland China
# via: https://github.com/JixunMoe/unblock-163/issues/3#issuecomment-163115880
for host in ( ' http://m5.music.126.net ' , ' http://115.231.74.139/m1.music.126.net ' ,
' http://124.40.233.182/m1.music.126.net ' , ' http://203.130.59.9/m1.music.126.net ' ) :
song_url = host + song_file_path
bitrate = int_or_none ( details . get ( ' bitrate ' ) ) or 999000
data = self . _call_player_api ( song_id , bitrate )
for song in try_get ( data , lambda x : x [ ' data ' ] , list ) or [ ] :
song_url = try_get ( song , lambda x : x [ ' url ' ] )
if not song_url :
continue
if self . _is_valid_url ( song_url , info [ ' id ' ] , ' song ' ) :
formats . append ( {
' url ' : song_url ,
' ext ' : details . get ( ' extension ' ) ,
' abr ' : float_or_none ( details . get ( ' bitrate ' ) , scale = 1000 ) ,
' abr ' : float_or_none ( song . get ( ' b r' ) , scale = 1000 ) ,
' format_id ' : song_format ,
' filesize ' : details . get ( ' size ' ) ,
' asr ' : details . get ( ' sr ' )
' filesize ' : int_or_none( song . get ( ' size ' ) ) ,
' asr ' : int_or_none( details. get ( ' sr ' ) ) ,
} )
break
elif err == 0 :
err = try_get ( song , lambda x : x [ ' code ' ] , int )
if not formats :
msg = ' No media links found '
if err != 0 and ( err < 200 or err > = 400 ) :
raise ExtractorError (
' %s (site code %d ) ' % ( msg , err , ) , expected = True )
else :
self . raise_geo_restricted (
msg + ' : probably this video is not available from your location due to geo restriction. ' ,
countries = [ ' CN ' ] )
return formats
@classmethod
@ -67,33 +154,19 @@ class NetEaseMusicBaseIE(InfoExtractor):
class NetEaseMusicIE ( NetEaseMusicBaseIE ) :
IE_NAME = ' netease:song '
IE_DESC = ' 网易云音乐 '
_VALID_URL = r ' https?:// music\ .163 \ .com/( #/)?song\ ? id=(?P<id>[0-9]+)'
_VALID_URL = r ' https?:// (y\ .)? music\ .163 \ .com/( ?:[ #m] /)?song\ ? .*?\ b id=(?P<id>[0-9]+)'
_TESTS = [ {
' url ' : ' http://music.163.com/#/song?id=32102397 ' ,
' md5 ' : ' f2e97280e6345c74ba9d5677dd5dcb45 ' ,
' md5 ' : ' 3e909614ce09b1ccef4a3eb205441190 ' ,
' info_dict ' : {
' id ' : ' 32102397 ' ,
' ext ' : ' mp3 ' ,
' title ' : ' Bad Blood (feat. Kendrick Lamar) ' ,
' title ' : ' Bad Blood ' ,
' creator ' : ' Taylor Swift / Kendrick Lamar ' ,
' upload_date ' : ' 20150517 ' ,
' timestamp ' : 1431878400 ,
' description ' : ' md5:a10a54589c2860300d02e1de821eb2ef ' ,
} ,
' skip ' : ' Blocked outside Mainland China ' ,
} , {
' note ' : ' No lyrics translation. ' ,
' url ' : ' http://music.163.com/#/song?id=29822014 ' ,
' info_dict ' : {
' id ' : ' 29822014 ' ,
' ext ' : ' mp3 ' ,
' title ' : ' 听见下雨的声音 ' ,
' creator ' : ' 周杰伦 ' ,
' upload_date ' : ' 20141225 ' ,
' timestamp ' : 1419523200 ,
' description ' : ' md5:a4d8d89f44656af206b7b2555c0bce6c ' ,
' upload_date ' : ' 20150516 ' ,
' timestamp ' : 1431792000 ,
' description ' : ' md5:25fc5f27e47aad975aa6d36382c7833c ' ,
} ,
' skip ' : ' Blocked outside Mainland China ' ,
} , {
' note ' : ' No lyrics. ' ,
' url ' : ' http://music.163.com/song?id=17241424 ' ,
@ -103,9 +176,9 @@ class NetEaseMusicIE(NetEaseMusicBaseIE):
' title ' : ' Opus 28 ' ,
' creator ' : ' Dustin O \' Halloran ' ,
' upload_date ' : ' 20080211 ' ,
' description ' : ' md5:f12945b0f6e0365e3b73c5032e1b0ff4 ' ,
' timestamp ' : 1202745600 ,
} ,
' skip ' : ' Blocked outside Mainland China ' ,
} , {
' note ' : ' Has translated name. ' ,
' url ' : ' http://music.163.com/#/song?id=22735043 ' ,
@ -119,7 +192,18 @@ class NetEaseMusicIE(NetEaseMusicBaseIE):
' timestamp ' : 1264608000 ,
' alt_title ' : ' 说出愿望吧(Genie) ' ,
} ,
' skip ' : ' Blocked outside Mainland China ' ,
} , {
' url ' : ' https://y.music.163.com/m/song?app_version=8.8.45&id=95670&uct2=sKnvS4+0YStsWkqsPhFijw % 3D % 3D&dlt=0846 ' ,
' md5 ' : ' 95826c73ea50b1c288b22180ec9e754d ' ,
' info_dict ' : {
' id ' : ' 95670 ' ,
' ext ' : ' mp3 ' ,
' title ' : ' 国际歌 ' ,
' creator ' : ' 马备 ' ,
' upload_date ' : ' 19911130 ' ,
' timestamp ' : 691516800 ,
' description ' : ' md5:1ba2f911a2b0aa398479f595224f2141 ' ,
} ,
} ]
def _process_lyrics ( self , lyrics_info ) :