虾米音乐在线试听高音质下载地址获取原理

上一篇文章中我给出了Node.js 虾米音乐高音质MP3批量下载脚本,可以用来下载VIP试听时的高音质音乐,但我认为虾米会修改音乐的播放地址,所以我将下载地址获取的原理发布出来,其实里面也没有什么高深的技术,需要时可以自己用用(也可以通过这个原理做出网站,批量转换出下载地址然后供他人下载).

**感谢JimmyZ提供方便下载的bookmarklet:

javascript:(function(n){function r(e,d){var h=new XMLHttpRequest;h.open('GET','/song/gethqsong/sid/'+e,!0);h.onreadystatechange=function(){if(4==h.readyState){var f;var a=JSON.parse(h.responseText).location;try{for(var b=void 0,c=Number(a.charAt(0)),e=a.substring(1),k=Math.floor(e.length/c),l=e.length%c,g=[],a=0;a<l;)void 0==g[a]&&(g[a]=''),g[a]=e.substr((k+1)*a,k+1),a+=1;for(a=l;a<c;)g[a]=e.substr(k*(a-l)+(k+1)*l,k),a+=1;c='';for(a=0;a<g[0].length;){for(b=0;b<g.length;)c+=g[b].charAt(a),b+=1;a+=1}c=unescape(c);b='';for(a=0;a<c.length;)b='^'==c.charAt(a)?b+'0':b+c.charAt(a),a+=1;f=b=b.replace('+',' ')}catch(m){f=!1}d(f)}};h.send()}for(var p=n.getElementsByTagName('a'),q={},m=0;m<p.length;++m)(function(e){var d=/^\/song\/(\d+)/.exec(e.getAttribute('href'));null!==d&&(d=d[1],q[d]||(q[d]=!0,r(d,function(d){var f=n.createElement('a');f.setAttribute('href',d);f.setAttribute('download',e.textContent.trim()+'.mp3');f.click()})))})(p[m])})(document);

原理

首先你必须搞到一个虾米VIP帐号,现在虾米有个使用虾米音乐App获取2个月VIP的活动,如果你看到这篇文章时活动已经结束,那么你只好自己充值VIP或者参与虾米相关的其他活动(如果有的话),向你的高富帅朋友借一个帐号也行.

得到一个帐号后登录,用浏览器访问http://www.xiami.com/vip/myvip,确认音质设置为”高音质”,然后找到你想要下载的歌并播放它(请使用Chrome浏览器).

点击上方的黑条,然后按F12调出开发者工具,点击界面上的Network选项卡.

把歌曲切换到下一首然后再切换回来(目的是找出音乐文件),你会看到一列文件请求,其中排在后面且文件大小比较大的就是这首音乐的文件了.

现在高音质的文件基本上都来自m3.file.xiami.com这个域名,而低音质的文件来自m1.file.xiami.com域名.一些音乐由于没有高音质,所以还是m1域名的.

点击文件名,会显示详细信息.

其中Request URL就是下载地址.

下载这个文件.

由于文件名是数字和字母的组合,所以我们把它修改成音乐原来的名称,并加上.mp3后缀.

使用MediaInfo查看,可以发现该文件是320Kbps的高音质音乐.

普通用户使用VIP下载试听音乐的原理就到此为止了,下面的内容稍微有些偏技术,可以不用看了.

随便打开一个单曲的网页,比如这首A Little Story,检查它的源代码,可以发现点击”立即播放”的时候,会调用一个play函数.

play('1770040300');

在虾米网的这个JS文件中可以找到play函数:

function play(song_id,type_name,type_id){
        if(!type_name) type_name  ='default';
        if(!type_id)type_id = 0;
        addSongs(escape("/song/playlist/id/"+song_id+"/object_name/"+type_name+"/object_id/"+type_id));
};

可以发现函数的参数song_id就是网页URL上的那串数字(sid),随后会将几个参数组合成一串URL:

"/song/playlist/id/1771779092/object_name/default/object_id/0"

这串URL经过escape编码后将作为参数传给addSongs函数.

addSongs函数是这样的:

function addSongs(str){
        thisMovie('trans').addSongs(str);
}

在addSongs函数中,使用到了thisMovie(‘trans’)这个对象,继续找到thisMovie函数:

function thisMovie(movieName){
    return window.document[movieName] || document.getElementById(movieName);
    // if (navigator.appName.indexOf("Microsoft") != -1) {
        // if(navigator.appVersion.match(/9./i)!="9."){
            // return window[movieName];
        // }else{
            // return document[movieName];
        // }
    // } else {
        // return document[movieName];
    // }
    //return navigator.appName.indexOf("Microsoft") != -1 ? window[movieName] : document[movieName];
};

thisMovie(‘trans’)现在真相大白了,它就是一个Flash对象:

<embed src="/res/player/sdtos.swf?v=2012111602" width="1" height="1" quality="high" wmode="transparent" allowscriptaccess="sameDomain" pluginspage="http://www.adobe.com/shockwave/download/download.cgi?P1_Prod_Version=ShockwaveFlash" type="application/x-shockwave-flash" name="trans">

现在我们只知道每首音乐都有一个sid,但是却不知道它和下载地址有什么关系,而且在网页的源代码中找不到这种对应关系.

通过查看Flash播放器的网络,可以监听到一个URL地址为http://www.xiami.com/song/gethqsong/sid/1770040300的请求,那串数字就是sid了,和单曲网页的URL上的是一样的,访问该地址,会得到一个JSON数据,其中location似乎是有用的部分.

反编译播放器的swf文件,找到一段关于高音质的AS代码:

package xiami

{

    import com.adobe.serialization.json.*;

    import flash.events.*;

    import flash.net.*;

    public class HQlocationDecode extends EventDispatcher

    {

        private var _url:String;

        private var _isHQ:Boolean;

        private static const getHQlocationUrl:String = "http://www.xiami.com/song/gethqsong/sid/";

        private static const getTrylocationUrl:String = "http://www.xiami.com/song/gethqsong";

        public function HQlocationDecode()

        {

            return;

        }// end function

        public function getTryLoation(param1:int, param2:String = null, param3:int = 0) : void

        {

            var _loc_4:* = new URLRequest(getTrylocationUrl);

            var _loc_5:* = new URLVariables();

            _loc_5.sid = param1;

            _loc_5.type = param2;

            _loc_5.typeId = param3;

            _loc_4.data = _loc_5;

            var _loc_6:* = new URLLoader();

            _loc_6.addEventListener(Event.COMPLETE, loadCompleteHandler);

            _loc_6.addEventListener(IOErrorEvent.IO_ERROR, errorHandler);

            _loc_6.load(_loc_4);

            return;

        }// end function

        public function getHQLocation(param1:int, param2:String = null, param3:int = 0) : void

        {

            var _loc_4:* = new URLRequest(getHQlocationUrl + param1);

            var _loc_5:* = new URLLoader();

            _loc_5.addEventListener(Event.COMPLETE, loadCompleteHandler);

            _loc_5.addEventListener(IOErrorEvent.IO_ERROR, errorHandler);

            _loc_5.load(_loc_4);

            return;

        }// end function

        private function loadCompleteHandler(event:Event) : void

        {

            var _loc_4:* = undefined;

            var _loc_2:* = event.target.data;

            var _loc_3:* = JSON.decode(_loc_2).status;

            if (_loc_3 == 1)

            {

                _isHQ = true;

                _loc_4 = JSON.decode(_loc_2).location;

                _url = getLocation(_loc_4);

            }

            else

            {

                _isHQ = false;

                _url = null;

            }

            dispatchEvent(new Event(Event.COMPLETE));

            return;

        }// end function

        private function errorHandler(event:Event) : void

        {

            _isHQ = false;

            dispatchEvent(new IOErrorEvent(IOErrorEvent.IO_ERROR));

            return;

        }// end function

        public function get url() : String

        {

            return _url;

        }// end function

        public function get isHQ() : Boolean

        {

            return _isHQ;

        }// end function

        public function getLocation(param1:String) : String

        {

            var _loc_10:* = undefined;

            var _loc_2:* = Number(param1.charAt(0));

            var _loc_3:* = param1.substring(1);

            var _loc_4:* = Math.floor(_loc_3.length / _loc_2);

            var _loc_5:* = _loc_3.length % _loc_2;

            var _loc_6:* = new Array();

            var _loc_7:* = 0;

            while (_loc_7 < _loc_5)

            {

                if (_loc_6[_loc_7] == undefined)

                {

                    _loc_6[_loc_7] = "";

                }

                _loc_6[_loc_7] = _loc_3.substr((_loc_4 + 1) * _loc_7, (_loc_4 + 1));

                _loc_7 = _loc_7 + 1;

            }

            _loc_7 = _loc_5;

            while (_loc_7 < _loc_2)

            {

                _loc_6[_loc_7] = _loc_3.substr(_loc_4 * (_loc_7 - _loc_5) + (_loc_4 + 1) * _loc_5, _loc_4);

                _loc_7 = _loc_7 + 1;

            }

            var _loc_8:* = "";

            _loc_7 = 0;

            while (_loc_7 < _loc_6[0].length)

            {

                _loc_10 = 0;

                while (_loc_10 < _loc_6.length)

                {

                    _loc_8 = _loc_8 + _loc_6[_loc_10].charAt(_loc_7);

                    _loc_10 = _loc_10 + 1;

                }

                _loc_7 = _loc_7 + 1;

            }

            _loc_8 = unescape(_loc_8);

            var _loc_9:* = "";

            _loc_7 = 0;

            while (_loc_7 < _loc_8.length)

            {

                if (_loc_8.charAt(_loc_7) == "^")

                {

                    _loc_9 = _loc_9 + "0";

                }

                else

                {

                    _loc_9 = _loc_9 + _loc_8.charAt(_loc_7);

                }

                _loc_7 = _loc_7 + 1;

            }

            _loc_9 = _loc_9.replace("+", " ");

            return _loc_9;

        }// end function

    }

}

其中getLocation函数是有用的部分,将之前得到的location传入后即可获得音乐的下载地址.

sid和location和下载地址的关系已经明了:

  1. 在单曲页面的URL中得到SID
  2. 通过http://www.xiami.com/song/gethqsong/sid/要下载音乐的SID获得location(经测试location会动态变动,可能是虾米防止高音质地址流出的手段).
  3. 使用getLocation函数可以转换出下载地址

下载地址将会在一段时间内有效,我猜测即使今后虾米音乐更改相关的URL,获取下载地址的原理还是不会变的.并且只有在你使用VIP帐号时的cookie访问http://www.xiami.com/song/gethqsong/sid/时才能获取到高音质的location,所以非VIP用户想要通过试听的途径获取到高音质音乐是不可能了(如果虾米不限制IP和Cookie下载的话,我在全文第一段写的那个括号或许会派上用场).