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

本文为《Node.js 虾米音乐高音质MP3批量下载脚本 第二版》中所提供下载脚本的新版.

更新内容

  • 新增 下载专辑,在启动参数中使用专辑页面的地址即可.
  • 新增 精选集和专辑将会自动保存到以精选集或专辑命名的文件夹中.

已知问题

  • 同时下载的数量过多时可能导致脚本卡在最后一个下载, 暂时的解决方法是终止脚本后重新下载(已下载的会被跳过).

使用方法

与第一版相同.

脚本代码

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;  
var total = 0,  
    count = 0;

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) {  
    try {
        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;
    } catch (e) {
        return false;
    }
}

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 tryAgain(pageUrl) {  
    timers.setTimeout(function() {
        download(pageUrl)
    }, 5 * 1000);
}

function showProgress() {  
    info('进度[' + count + '/' + total + ']');
}

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 ? (' - ' + artist) : '') + '.mp3' //+' - '+album+'.mp3';
            if ((title || artist) && title.indexOf('span class') < 0) {
                filename = filename.replace('<span>', ' ');
                filename = filename.replace('</span>', '');
                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);
                        if (location) {
                            var fullFilename = path.resolve(path.join(__dirname, filename));
                            fs.exists(fullFilename, function(exists) {
                                var req = http.get(location, function(res) {
                                    function save() {
                                        var f = fs.createWriteStream(fullFilename);
                                        info('准备下载: ' + filename);
                                        res.pipe(f);
                                    }
                                    var text = '下载完毕';
                                    if (exists) {
                                        var contentLength = parseInt(res.headers['content-length']);
                                        var stat = fs.statSync(fullFilename);
                                        if (stat.size == contentLength) {
                                            text = '已经存在';
                                            req.abort();
                                        } else {
                                            save();
                                        }
                                    } else {
                                        save();
                                    }
                                    res.on('error', function(err) {
                                        info('未知错误: ' + err);
                                    });
                                    res.on('end', function() {
                                        count++;
                                        info(text + ': ' + filename);
                                        showProgress();
                                    });
                                });
                            })
                        } else {
                            count++;
                            info('该音乐暂未发布: ' + pageUrl);
                            showProgress();
                        }
                    });
                });
            } else {
                tryAgain(pageUrl);
            }
        });
    })
}

function main() {  
    for (var i = 0; i < urlList.length; i++) {
        (function() {
            var pageUrl = urlList[i];
            if (isSong.test(pageUrl)) {
                total++;
                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)) {
                            total++;
                            download(url.resolve('http://www.xiami.com', result[1]));
                        }
                    });
                })
            } else {
                info('未知类型的URL: ' + pageUrl);
            }
        })();
    }
}

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

process.on('uncaughtException', function(err) {  
    info('意外错误: ' + err);
});