@ -1,12 +1,25 @@
import itertools
import json
import re
import re
import time
from base64 import b64encode
from base64 import b64encode
from binascii import hexlify
from datetime import datetime
from datetime import datetime
from hashlib import md5
from hashlib import md5
from random import randint
from . common import InfoExtractor
from . common import InfoExtractor
from . . compat import compat_str , compat_urllib_parse_urlencode
from . . aes import aes_ecb_encrypt , pkcs7_padding
from . . utils import float_or_none , sanitized_Request
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 ) :
class NetEaseMusicBaseIE ( InfoExtractor ) :
@ -17,7 +30,7 @@ class NetEaseMusicBaseIE(InfoExtractor):
@classmethod
@classmethod
def _encrypt ( cls , dfsid ) :
def _encrypt ( cls , dfsid ) :
salt_bytes = bytearray ( cls . _NETEASE_SALT . encode ( ' utf-8 ' ) )
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 )
salt_len = len ( salt_bytes )
for i in range ( len ( string_bytes ) ) :
for i in range ( len ( string_bytes ) ) :
string_bytes [ i ] = string_bytes [ i ] ^ salt_bytes [ i % salt_len ]
string_bytes [ i ] = string_bytes [ i ] ^ salt_bytes [ i % salt_len ]
@ -26,32 +39,106 @@ class NetEaseMusicBaseIE(InfoExtractor):
result = b64encode ( m . digest ( ) ) . decode ( ' ascii ' )
result = b64encode ( m . digest ( ) ) . decode ( ' ascii ' )
return result . replace ( ' / ' , ' _ ' ) . replace ( ' + ' , ' - ' )
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 ) :
def extract_formats ( self , info ) :
err = 0
formats = [ ]
formats = [ ]
song_id = info [ ' id ' ]
for song_format in self . _FORMATS :
for song_format in self . _FORMATS :
details = info . get ( song_format )
details = info . get ( song_format )
if not details :
if not details :
continue
continue
song_file_path = ' / %s / %s . %s ' % (
self . _encrypt ( details [ ' dfsId ' ] ) , details [ ' dfsId ' ] , details [ ' extension ' ] )
bitrate = int_or_none ( details . get ( ' bitrate ' ) ) or 999000
data = self . _call_player_api ( song_id , bitrate )
# 203.130.59.9, 124.40.233.182, 115.231.74.139, etc is a reverse proxy-like feature
for song in try_get ( data , lambda x : x [ ' data ' ] , list ) or [ ] :
# from NetEase's CDN provider that can be used if m5.music.126.net does not
song_url = try_get ( song , lambda x : x [ ' url ' ] )
# work, especially for users outside of Mainland China
if not song_url :
# via: https://github.com/JixunMoe/unblock-163/issues/3#issuecomment-163115880
continue
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
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 ' ) ,
' 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 ,
' format_id ' : song_format ,
' filesize ' : details . get ( ' size ' ) ,
' filesize ' : int_or_none( song . get ( ' size ' ) ) ,
' asr ' : details . get ( ' sr ' )
' 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
return formats
@classmethod
@classmethod
@ -67,33 +154,19 @@ class NetEaseMusicBaseIE(InfoExtractor):
class NetEaseMusicIE ( NetEaseMusicBaseIE ) :
class NetEaseMusicIE ( NetEaseMusicBaseIE ) :
IE_NAME = ' netease:song '
IE_NAME = ' netease:song '
IE_DESC = ' 网易云音乐 '
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 = [ {
_TESTS = [ {
' url ' : ' http://music.163.com/#/song?id=32102397 ' ,
' url ' : ' http://music.163.com/#/song?id=32102397 ' ,
' md5 ' : ' f2e97280e6345c74ba9d5677dd5dcb45 ' ,
' md5 ' : ' 3e909614ce09b1ccef4a3eb205441190 ' ,
' info_dict ' : {
' info_dict ' : {
' id ' : ' 32102397 ' ,
' id ' : ' 32102397 ' ,
' ext ' : ' mp3 ' ,
' ext ' : ' mp3 ' ,
' title ' : ' Bad Blood (feat. Kendrick Lamar) ' ,
' title ' : ' Bad Blood ' ,
' creator ' : ' Taylor Swift / Kendrick Lamar ' ,
' creator ' : ' Taylor Swift / Kendrick Lamar ' ,
' upload_date ' : ' 2015051 7 ' ,
' upload_date ' : ' 2015051 6 ' ,
' timestamp ' : 1431 8784 00,
' timestamp ' : 1431 7920 00,
' description ' : ' md5: a10a54589c2860300d02e1de821eb2ef ' ,
' description ' : ' md5: 25fc5f27e47aad975aa6d36382c7833c ' ,
} ,
} ,
' 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 ' ,
} ,
' skip ' : ' Blocked outside Mainland China ' ,
} , {
} , {
' note ' : ' No lyrics. ' ,
' note ' : ' No lyrics. ' ,
' url ' : ' http://music.163.com/song?id=17241424 ' ,
' url ' : ' http://music.163.com/song?id=17241424 ' ,
@ -103,9 +176,9 @@ class NetEaseMusicIE(NetEaseMusicBaseIE):
' title ' : ' Opus 28 ' ,
' title ' : ' Opus 28 ' ,
' creator ' : ' Dustin O \' Halloran ' ,
' creator ' : ' Dustin O \' Halloran ' ,
' upload_date ' : ' 20080211 ' ,
' upload_date ' : ' 20080211 ' ,
' description ' : ' md5:f12945b0f6e0365e3b73c5032e1b0ff4 ' ,
' timestamp ' : 1202745600 ,
' timestamp ' : 1202745600 ,
} ,
} ,
' skip ' : ' Blocked outside Mainland China ' ,
} , {
} , {
' note ' : ' Has translated name. ' ,
' note ' : ' Has translated name. ' ,
' url ' : ' http://music.163.com/#/song?id=22735043 ' ,
' url ' : ' http://music.163.com/#/song?id=22735043 ' ,
@ -119,7 +192,18 @@ class NetEaseMusicIE(NetEaseMusicBaseIE):
' timestamp ' : 1264608000 ,
' timestamp ' : 1264608000 ,
' alt_title ' : ' 说出愿望吧(Genie) ' ,
' 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 ) :
def _process_lyrics ( self , lyrics_info ) :