Greasy Fork

Shined up real nice.

Greasy Fork is available in English.

网易云音乐高音质支持

去除网页版网易云音乐仅可播放低音质(96Kbps)的限制,强制播放高音质版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
// ==UserScript==
// @name         网易云音乐高音质支持
// @version      3.3
// @description  去除网页版网易云音乐仅可播放低音质(96Kbps)的限制,强制播放高音质版本
// @match        *://music.163.com/*
// @include      *://music.163.com/*
// @author       864907600cc
// @icon         https://secure.gravatar.com/avatar/147834caf9ccb0a66b2505c753747867
// @run-at       document-start
// @grant        none
// @namespace    http://ext.ccloli.com
// ==/UserScript==

// getTrackURL 源码来自 Chrome 扩展程序 网易云音乐增强器(Netease Music Enhancement) by wanmingtom@gmail.com
// 菊苣这个加密算法你是怎么知道的 _(:3
var getTrackURL = function getTrackURL (dfsId) {
    var byte1 = '3' + 'g' + 'o' + '8' + '&' + '$' + '8' + '*' + '3' + 
                '*' + '3' + 'h' + '0' + 'k' + '(' + '2' + ')' + '2';
    var byte1Length = byte1.length;
    var byte2 = dfsId + '';
    var byte2Length = byte2.length;
    var byte3 = [];
    for (var i = 0; i < byte2Length; i++) {
        byte3[i] = byte2.charCodeAt(i) ^ byte1.charCodeAt(i % byte1Length);
    }

    byte3 = byte3.map(function(i) {
        return String.fromCharCode(i);
    }).join('');

    results = CryptoJS.MD5(byte3).toString(CryptoJS.enc.Base64);
    results = results.replace(/\//g, '_').replace(/\+/g, '-');

    // 如果需要修改使用的 cdn,请修改下面的地址
    // 可用的服务器有 ['m1', 'm2', 'p1', 'p2']
    // 使用 p1 或 p2 的 cdn 可解决境外用户无法播放的问题
    var url = 'http://m1.music.126.net/' + results + '/' + byte2 + '.mp3';
    return url;
};

var modifyURL = function modifyURL(data, parentKey) {
    console.log('API 返回了 ' + data.length + ' 首曲目');
    console.log('施放魔法!变变变!');
    data.forEach(function(elem){
        // 部分音乐没有高音质
        if (!parentKey) elem.mp3Url = getTrackURL((elem.hMusic || elem.mMusic || elem.lMusic).dfsId);
        else elem[parentKey].mp3Url = getTrackURL((elem[parentKey].hMusic || elem[parentKey].mMusic || elem.lMusic[parentKey]).dfsId);
    });
    return data;
};

var cachedURL = {};
var qualityNode = null;

// 重新编写脚本,改用 hook xhr 的形式替换 URL 链接
var originalXMLHttpRequest = window.XMLHttpRequest;
var fakeXMLHttpRequest = function(){
    var __this__ = this;
    var xhr = new originalXMLHttpRequest();
    var xhrProto = xhr.constructor.prototype;

    Object.keys(xhrProto).forEach(function(elem){
    	if (elem in __this__) return;
        if (elem === 'responseText') return;

        if (typeof xhr[elem] === 'function') {
            if (elem === 'open') { // add requestURL support
                __this__[elem] = function(){
                    if (arguments[1].indexOf('/enhance/player/') >= 0) {
                        // 对新版 api 请求旧的 api 接口
                        __this__.ping = new fakeXMLHttpRequest();
                        __this__.ping.open(arguments[0], arguments[1].replace('/enhance/player/url', '/detail'), false); // 不使用异步 xhr 以阻断原 xhr 请求
                        __this__.ping.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
                    }
                    xhr[elem].apply(xhr, arguments);
                    __this__.requestURL = arguments[1];
                };
            }
            else if (elem === 'send') { // add requestURL support
                __this__[elem] = function(){
                    if (__this__.requestURL.indexOf('/enhance/player/') >= 0 && __this__.ping) {
                        //__this__.ping.send(arguments[0]);
                        __this__.ping.sendData = arguments[0];
                    }
                    xhr[elem].apply(xhr, arguments);
                };
            }
            else {
                __this__[elem] = function(){
                    xhr[elem].apply(xhr, arguments);
                };
            }
        }
        else {
            var property = {};
            var originalProperty = Object.getOwnPropertyDescriptor(xhrProto, elem);
            property.get = function(){ /*console.log(elem);*/ return xhr[elem]; };
            if (originalProperty.set) property.set = function(val){ return xhr[elem] = val; };
            Object.defineProperty(__this__, elem, property);
        }
    });

    Object.defineProperty(__this__, 'responseText', {
        get: function(){
            try {
                if (__this__.requestURL.indexOf('/weapi/') < 0 && __this__.requestURL.indexOf('/api/') < 0) {
                    return xhr.responseText;
                }
                var action = __this__.requestURL.split(/\/(?:we)?api\//)[1].split('?')[0].split('/');
                var res = JSON.parse(xhr.responseText);

                switch (action[0]) {
                    case 'album':
                        modifyURL(res.album.songs);
                        return JSON.stringify(res);
                        
                    case 'song':
                        if (action[1] !== 'detail') {
                            if (action[1] !== 'enhance' && action[2] !== 'player' && action[3] !== 'url') {
                                return xhr.responseText;
                            }

                            // 给黄易节省带宽,优先调用缓存的 URL
                            if (cachedURL[res.data[0].id] && new Date() < cachedURL[res.data[0].id].expires) {
                                res.data[0].url = cachedURL[res.data[0].id].url;
                                delete __this__.ping;
                                if (qualityNode) {
                                    qualityNode.textContent = cachedURL[res.data[0].id].quality / 1000 + 'K';
                                }
                                return JSON.stringify(res);
                            }
                            
                            // 缓存超时了再用新的 URL
                            // 其实实际 CDN 超时实践并不是那个 `expi`,而是 URL 的 path 的第一个字段
                            // 不过不知道后期会不会改变,以及这个 path 的时间戳可能是基于 GMT+8
                            // 为了减少复杂度,就用 `expi` 了,不过实际上真正的可用时间是远高于 `expi` 的

                            // 因为新版 API 已经返回的是高音质版本,所以不需要再请求旧的 API
                            // 如果返回地址为 null 再尝试获取旧版 API
                            if (res.data[0].url) {
                                cachedURL[res.data[0].id] = {
                                    url: res.data[0].url,
                                    quality: res.data[0].br,
                                    expires: res.data[0].expi * 1000 + new Date().getTime()
                                };
                                delete __this__.ping;
                                if (qualityNode) {
                                    qualityNode.textContent = (res.data[0].br) / 1000 + 'K';
                                }
                                return xhr.responseText;
                            }

                            if (!cachedURL[res.data[0].id]) { // 若未缓存,且新 API 没有音质再请求原始 api
                                console.log('新版 API 未返回 URL,fallback 至旧版 API');
                                __this__.ping.send(__this__.ping.sendData);
                                // 因为使用了同步 xhr 所以请求会被阻塞,下面的代码相当于回调
                                // 其实获取到 pingRes 时已经是对本函数的一次执行了,qualityNode 不需要再更改
                                var pingRes = JSON.parse(__this__.ping.responseText);
                                var pingResSong = pingRes.songs[0];
                                cachedURL[res.data[0].id] = {
                                    url: pingRes.songs[0].mp3Url,
                                    quality: (pingResSong.hMusic || pingResSong.h || pingResSong.mMusic || pingResSong.m || pingResSong.lMusic || pingResSong.l).bitrate,
                                    expires: Infinity // 旧版 API URL 永不超时
                                };
                            }
                            res.data[0].url = cachedURL[res.data[0].id].url;
                            return JSON.stringify(res);
                        }
                        
                        // 这里是处理旧版 API 的部分
                        modifyURL(res.songs);
                        if (qualityNode) {
                            qualityNode.textContent = (res.songs[0].hMusic || res.songs[0].h || res.songs[0].mMusic || res.songs[0].m || res.songs[0].lMusic || res.songs[0].l).bitrate / 1000 + 'K';
                        }
                        return JSON.stringify(res);
                        
                    case 'playlist':
                        if (action[1] !== 'detail') {
                            return xhr.responseText;
                        }
                        
                        modifyURL(res.result.tracks);
                        return JSON.stringify(res);
                        
                    case 'dj':
                        if (action[2] === 'byradio') {
                            modifyURL(res.programs, 'mainSong');
                            return JSON.stringify(res);
                        }
                        if (action[2] === 'detail') {
                            res.program = modifyURL([res.program], 'mainSong')[0];
                            return JSON.stringify(res);
                        }
                        return xhr.responseText;

                    case 'radio':
                        if (action[1] === 'get') {
                            modifyURL(res.data);
                            return JSON.stringify(res);
                        }
                        return xhr.responseText;
                        
                    case 'v3':
                        switch (action[1]){
                            // http://music.163.com/weapi/v3/playlist/detail
                            case 'playlist':
                                if (action[2] !== 'detail') {
                                    return xhr.responseText;
                                }

                                res.privileges.forEach(function(elem){
                                    var q = elem.pl || elem.dl || elem.fl || Math.min(elem.maxbr, 320000) || 320000;
                                    elem.st = 0;
                                    elem.pl = q;
                                    elem.dl = q;
                                    elem.fl = q;
                                });
                                if (res.privileges.length < res.playlist.trackIds.length && res.playlist.trackIds.length === res.playlist.tracks.length) {
                                    // 对超过 1000 的播放列表补充播放信息(需魔改 core.js)
                                    for (var i = res.privileges.length; i < res.playlist.trackIds.length; i++) {
                                        var q = (res.playlist.tracks.h || res.playlist.tracks.m || res.playlist.tracks.l || res.playlist.tracks.a).br || 320000;
                                        res.privileges.push({
                                            cp: 1,
                                            cs: false,
                                            dl: q,
                                            fee: 0,
                                            fl: q,
                                            id: res.playlist.trackIds[i].id,
                                            maxbr: q,
                                            payed: 0,
                                            pl: q,
                                            sp: 7,
                                            st: 0,
                                            subp: 1
                                        });
                                    }
                                }
                                return JSON.stringify(res);
                                
                            case 'song':
                                if (action[2] !== 'detail') {
                                    return xhr.responseText;
                                }

                                res.privileges.forEach(function(elem){
                                    var q = elem.pl || elem.dl || elem.fl || Math.min(elem.maxbr, 320000) || 320000;
                                    elem.st = 0;
                                    elem.pl = q;
                                    elem.dl = q;
                                    elem.fl = q;
                                });
                                return JSON.stringify(res);
                                
                            default:
                                return xhr.responseText;
                        }
                        break;
                    default:
                        return xhr.responseText;
                }
            }
            catch (error) {
                // 以防 api 转换失败也能正常返回数据
                console.error('转换出错!', error);
                return xhr.responseText;
            }
        }
    });

    // 轮询当前对象的 prototype,以解决无法获取更高原型链的属性的问题
    var curPrototype = xhrProto;
    while (curPrototype = Object.getPrototypeOf(curPrototype)) {
        Object.keys(curPrototype).forEach(function(elem){
            var property = {};
            var originalProperty = Object.getOwnPropertyDescriptor(curPrototype, elem);
            property.get = function(){ /*console.log(elem);*/ return xhr[elem]; };
            if (originalProperty.set) property.set = function(val){ return xhr[elem] = val; };
            Object.defineProperty(__this__, elem, property);
        });
    }
    
    this.originalXMLHttpRequest = xhr;
};
window.XMLHttpRequest = fakeXMLHttpRequest;

// 旧版 API 大部分曲目失效,故 hook 加密函数以获取新版 API 的高音质版本
var original_asrsea;
var fake_asrsea = function(){
    //console.log(arguments)
    var data = JSON.parse(arguments[0]);
    if (data.br && data.br === 128000) {
        data.br = 320000;
        arguments[0] = JSON.stringify(data);
    }
    //console.log(arguments);
    return original_asrsea.apply(window, arguments);
};
if (window.asrsea) {
    original_asrsea = window.asrsea;
    window.asrsea = fake_asrsea;
}
else {
    Object.defineProperty(window, 'asrsea', {
        get: function(){
            return fake_asrsea;
        },
        set: function(val) {
            original_asrsea = val;
        }
    });
}


var quailtyInsertHandler = function() {
    var target = document.querySelector('.m-pbar');
    if (target) {
        qualityNode = document.createElement('span');
        qualityNode.style.cssText = 'color: #797979; position: absolute; top: 0px; right: 31px; text-shadow: 0 1px 0 #171717; line-height: 28px;';
        target.parentElement.insertBefore(qualityNode, target);
    }

    document.removeEventListener('DOMContentLoaded', quailtyInsertHandler, false);
};

document.addEventListener('DOMContentLoaded', quailtyInsertHandler, false);