Node.js 虾米音乐高音质MP3批量下载脚本

Node.js 虾米音乐高音质MP3批量下载脚本 第二版》已发布

现在每个用户都可以得到2个月的虾米VIP了,VIP虽然可以免费下载200首歌,但对于一些人来说还是不够用的.

好在VIP在试听时就可以得到比普通用户更高音质的音乐,普通用户大多数只能听到96kbps~128kbps的音乐,而VIP试听时却是接近320kbps的高品质音乐(一些音乐没有320kbps版本时还是播放96kbps的).

于是抓取试听音乐成为了突破下载量限制的手段,也是本脚本的实现原理所在,详细的分析将放在下一篇文章中.

编写这个脚本时我是用Node.js v0.10.10进行调试的,所以在其他版本中可能不可用,并且随着时间的推移,虾米很可能修改试听音乐的播放地址,故本人对本脚本造成的一切问题概不负责.

在启动脚本前,需要编辑前三行,填写你的虾米帐号密码(必须是VIP用户),cookie可以为空.

首次使用请登录http://www.xiami.com/vip/myvip确认自己帐号的音质已设置为”高音质”.

脚本支持下载单曲和精选集,将相关页面的网址作为参数启动脚本即可,例如:

node xiamiDownload.js http://www.xiami.com/song/showcollect/id/21723199

脚本代码如下,请保存为xiamiDownload.js:

var email = '',  
    password = '',
    cookie = '';

var url = require('url'),  
    http = require('http'),
    fs = require('fs'),
    querystring = require('querystring'),
    timers = require('timers'),
    path = require('path');

var sidPattern = /(\d+)/,  
    songUrlPattern = /a href="(\/song\/\d+)"/g;
var isShowcollect = /www.xiami.com\/song\/showcollect\/id\/\d+/,  
    isSong = /www.xiami.com\/song\/\d+/;
var banChar = ['/', '\\', ':', '*', '?', '"', '<', '>', '|'];

var titlePattern = /<div id="title">\s*<h1>(.*)<\/h1>/,  
    albumPattern = /<a href="\/album\/\d+" title=".*">(.*)<\/a>/,
    artistPattern = /<a href="\/artist\/\d+" title=".*">(.*)<\/a>/;

var inputPattern = /input.+name="([^"]*)".+value="([^"]*)"/g,  
    inputPatternR = /input.+value="([^"]*)".+name="([^"]*)"/g;

var urlList = process.argv.slice(2, process.argv.length);

var info = console.log;

function reqHeaders(obj) {  
    var headers = {
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        //'Accept-Encoding':'gzip,deflate,sdch',
        'Accept-Language': 'zh-CN,zh;q=0.8',
        'Cache-Control': 'max-age=0',
        'Connection': 'keep-alive',
        'Host': 'www.xiami.com',
        'Origin': 'http://www.xiami.com',
        'User-Agent': 'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36 AlexaToolbar/alxg-3.1',
    }
    for (i in obj) {
        headers[i] = obj[i];
    }
    return headers;
}

function getLocation(param1) {  
    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;
}

function login(email, password, callback) {  
    var form = {};
    http.get('http://www.xiami.com/member/login', function(res) {
        res.setEncoding('utf8');
        var html = '';
        res.on('data', function(data) {
            html += data;
        });
        res.on('end', function() {
            while (result = inputPattern.exec(html)) {
                form[result[1]] = result[2];
            }
            while (result = inputPatternR.exec(html)) {
                form[result[2]] = result[1];
            }
            form['email'] = email;
            form['password'] = password;
            form = querystring.stringify(form);

            var options = {
                host: 'www.xiami.com',
                path: 'http://www.xiami.com/member/login',
                method: 'POST',
                headers: reqHeaders({
                    'Content-Length': form.length,
                    'Referer': 'http://www.xiami.com/member/login',
                    'Content-Type': 'application/x-www-form-urlencoded'
                })
            }
            var req = http.request(options, function(res) {
                res.setEncoding('utf8');
                cookie = res.headers['set-cookie'];
                if (cookie) {
                    info('Cookie:' + cookie);
                    callback();
                }
            });
            req.write(form);
            req.end();
        });
    });
}

function download(pageUrl) {  
    var sid = sidPattern.exec(pageUrl)[1];
    var options = url.parse(pageUrl);
    options.headers = reqHeaders({
        'Cookie': cookie
    });
    http.get(options, function(res) {
        res.setEncoding('utf8');
        var html = '';
        res.on('data', function(data) {
            html += data;
        });
        res.on('end', function() {
            var title = titlePattern.exec(html),
                //album=albumPattern.exec(html),
                artist = artistPattern.exec(html);
            title = title ? title[1] : null;
            artist = artist ? artist[1] : null;
            var filename = title + ' - ' + artist + '.mp3' //+' - '+album+'.mp3';
            if ((title || artist) && title.indexOf('span class') < 0) {
                filename = filename.replace(/(\/|\\|\:|\*|\?|\"|\<|\>|\||\s+)/g, ' ');
                options = url.parse('http://www.xiami.com/song/gethqsong/sid/' + sid);
                options.headers = reqHeaders({
                    'Cookie': cookie
                });
                http.get(options, function(res) {
                    res.setEncoding('utf8');
                    res.on('data', function(data) {
                        var location = getLocation(JSON.parse(data).location);
                        var f = fs.createWriteStream(path.resolve(path.join(__dirname, filename)));
                        http.get(location, function(res) {
                            info('开始下载: ' + filename);
                            res.pipe(f);
                            res.on('error', function(err) {
                                info('未知错误: ' + err);
                            });
                            res.on('end', function() {
                                info('下载完毕: ' + filename);
                            });
                        });
                    });
                });
            } else {
                timers.setTimeout(function() {
                    download(pageUrl)
                }, 5 * 1000);
            }
        });
    })
}

function main() {  
    for (var i = 0; i < urlList.length; i++) {
        (function() {
            var pageUrl = urlList[i];
            if (isSong.test(pageUrl)) {
                download(pageUrl);
            } else if (isShowcollect.test(pageUrl)) {
                var options = url.parse(pageUrl);
                http.get(options, function(res) {
                    res.setEncoding('utf8');
                    var html = '';
                    res.on('data', function(data) {
                        html += data;
                    });
                    res.on('end', function() {
                        var result;
                        while (result = songUrlPattern.exec(html)) {
                            download(url.resolve('http://www.xiami.com', result[1]));
                        }
                    });
                })
            } else {
                info('未知类型的URL: ' + pageUrl);
            }
        })();
    }
}

if (cookie) {  
    main();
} else {
    info('正在登录...');
    login(email, password, main);
}