Complete 163 Music Dl Helper

Download music from music.163.com

// ==UserScript==
// @name         Complete 163 Music Dl Helper
// @namespace    org.xryl.music.163
// @version      1.3
// @description  Download music from music.163.com
///
//               火狐匹配地址
// @include      http://music.163.com/#*
///
//               TamperMonkey 匹配地址
// @include      http://music.163.com/
///
// @copyright    2014+, Jixun.Moe, 2015+, X-Ryl669
// @run-at       document-start
// @grant        GM_download
// ==/UserScript==


(function () {
	// I love prototype.
	String.prototype.format = function () {
		var $arg = arguments,
			$len = $arg.length;
		return this.replace (/\{(\d+)\}/g, function (z, num) {
			num = parseInt (num);
			if (num < $len) {
				return $arg[num];
			} else {
				return z;
			}
		});
	};

    var $format = '{0}.[{1}] {2} - {3}'; // 0 is song index, 1 is album name, 2 is artist, 3 is song title

	var $bank = {};
	var $dlBtn = document.createElement ('a');
	$dlBtn.setAttribute ('target', '_blank');
	$dlBtn.setAttribute ('style', 'font-size:small;width:3em;left:initial;right:40px');
	$dlBtn.className = 'lytit';
	$dlBtn.textContent = 'down';
	$dlBtn.addEventListener ('click', function (e) {
		var $addr = this.getAttribute ('href');
        GM_download($addr, this.getAttribute('download'));
        e.preventDefault();
		e.stopPropagation ();
	}, false);
	var $dlAllBtn = document.createElement ('a');
	$dlAllBtn.setAttribute ('target', '_blank');
	$dlAllBtn.setAttribute ('style', 'font-size:small;width:3em;left:initial;right:80px');
	$dlAllBtn.className = 'lytit';
	$dlAllBtn.textContent = 'all';
	$dlAllBtn.addEventListener ('click', function (e) {
        var downOne = function(i) {
            console.log("Downloading '"+$playlist[i].name+"' from : "+$playlist[i].url);
            var detail = { url: $playlist[i].url, 
                           name: $playlist[i].name.replace(/\?/g, '\uFE56') + '.mp3', 
                           onload: function() { if ($playlist.length > i+1) setTimeout(function(){downOne(i+1);}, Math.random()*3000 + 3000); } 
                         };
            GM_download(detail);
        };
        if ($playlist.length) downOne(0);
        e.preventDefault();
		e.stopPropagation ();
	}, false);
    
    var $lastAlbum = '';
    var $playlist = [];

	var _updDownloadUrl = function (songId) {
		var $song = $bank[songId.toString()];
        var $info = $format.format (("0" + $song.index).slice(-2), $song.album, $song.artist, $song.name);
		console.info ('%s\n%s', $info, $song.url);
        
		$dlBtn.setAttribute ('title', 'Song: ' + $info);
        $dlBtn.setAttribute ('download', $info+'.mp3');
		$dlBtn.setAttribute ('href', $song.url);
	};
    
    var _mergeArtists = function(artists) {
        return artists.map(function (artist) {
				return artist.name;
		}).join (' & ');
    };

	var _addBank = function (songObj) {
        // Empty upon new album detection
        if (songObj.album.name != $lastAlbum) $bank = {};
        $lastAlbum = songObj.album.name;
		$bank[songObj.id.toString()] = {
			name: songObj.name,
			album: songObj.album.name,
			artist: _mergeArtists(songObj.artists),
			
			url: songObj.mp3Url,
            index: (Object.keys($bank).indexOf(songObj.id) == -1 ? Object.keys($bank).length : Object.keys($bank).indexOf(songObj.id)) + 1
		};
	};

    var _processURL = function($id, url) {
        if ($id == null) return;
        if ($bank.hasOwnProperty($id)) {
            _updDownloadUrl ($id);
        } else {
            // First, search through local cache.
            var cacheSongObj;
            try {
                cacheSongObj = JSON.parse(localStorage['track-queue']).filter (function (s) {
                    _addBank (s);
                    return $id == s.id.toString();
                });
            } catch (err) { }

            if (cacheSongObj) {
                console.info ('Music found from cache.');
                _updDownloadUrl (cacheSongObj.id);
            } else {
                console.info ('Requesting song information...');
                var $http = new window.XMLHttpRequest ();
                $http.onreadystatechange = function() {};
                $http.open(method, '/api/song/detail/?id=' + $id + '&ids=%5B' + $id + '%5D&csrf_token=' + url.match(/csrf_token=([^&]*)/)[1]);
                $http.send ();
            }
        }
    };

	window.XMLHttpRequest.prototype.oldOpen = window.XMLHttpRequest.prototype.open;
	window.XMLHttpRequest.prototype.open = function (method, url) {
		console.info ('[REQ] %s: %s', method, url);
		
        if (url.indexOf('/weapi/album/') != -1) {
			try {
				var $this = this;
				var bkReadystatechange = this.onreadystatechange;
				this.onreadystatechange = function (e) {
					if ($this.readyState === 4) {
						var album = JSON.parse($this.responseText.trim());
                        $playlist = [];
                        for (var i = 0; i < album.album.songs.length; i++) {
                            $playlist.push({
                                             url: album.album.songs[i].mp3Url, 
                                             name: $format.format (("0" + (i+1)).slice(-2), album.album.name, _mergeArtists(album.album.songs[i].artists), album.album.songs[i].name),
                                           });
                        }
                        console.log("Captured complete playlist for '"+album.album.name+"' " + $playlist.length+" tracks");
					}
					
					return bkReadystatechange.apply (this, arguments);
				};
			} catch (err) {
				console.error (err);
			}
        } else if (!url.indexOf('/api/song/media') || !url.indexOf('/api/song/lyric') || !url.indexOf('/weapi/song/lyric')) {
			console.log ('Check $bank...');
			
            var $id = url.indexOf('/api/song/media') === 0 ? url.match (/\d+/)[0] : 
                      url.indexOf('/api/song/lyric') === 0 ? url.match(/id=(\d+)/)[1] : 0;
            if (!url.indexOf('/weapi/song/lyric')) { 
                // Due to new change in their API, the ID is for the song no more part of the URL
                // So, let's extract from the page itself, AFTER it's refreshed
                setTimeout(function() { $id = document.querySelector("a.f-thide.name").getAttribute("href").match(/id=(\d+)/)[1]; _processURL($id, url); }, 2000);
            } else {
                _processURL($id, url);
            }
		} else if (-1 != url.indexOf('/api/song/detail')) {
			try {
				var $this = this;
				var bkReadystatechange = this.onreadystatechange;
				this.onreadystatechange = function (e) {
					if ($this.readyState === 4) {
						JSON.parse($this.responseText.trim()).songs.forEach(function (e) {
							_addBank (e);
							_updDownloadUrl (e.id);
						});
					}
					
					return bkReadystatechange.apply (this, arguments);
				};
			} catch (err) {
				console.error (err);
			}
        }
		
		return this.oldOpen.apply(this, arguments);
	};

	(function () {
		var $t;
		var vv = setInterval (function () {
			if (!($t = document.querySelector ('.lytit')))
				return ;
			clearInterval (vv);
			
			$t.style.maxWidth = '302px';
			$t.parentNode.insertBefore ($dlBtn, $t.nextElementSibling);
			$t.parentNode.insertBefore ($dlAllBtn, $t.nextElementSibling);
		}, 200);
	})();

})();