Skip to content

Commit e9bf7e2

Browse files
fix(hook): survive a refused response and a url-less entry (UnblockNeteaseMusic#1751)
Co-authored-by: Takoyaki White <staticwhite999@outlook.com> Signed-off-by: ITManCHINA <1678812512@qq.com>
1 parent 0ecebcd commit e9bf7e2

1 file changed

Lines changed: 107 additions & 11 deletions

File tree

‎src/hook.js‎

Lines changed: 107 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ const PLAIN_ENDPOINT_OS = new Set(
3232
.filter(Boolean)
3333
);
3434
const PACKAGE_SCHEME = (process.env.PACKAGE_SCHEME || '').toLowerCase();
35+
// Verdicts the upstream returns instead of a url when it refuses to serve the
36+
// track at all — a risk check tripping, rather than the track being unlicensed.
37+
// These arrive at the top level of the response, where a per-entry code would
38+
// normally be, and can leave the entry itself empty.
39+
const BLOCKED_CODES = new Set([-460, -462]);
3540

3641
const hook = {
3742
request: {
@@ -107,6 +112,7 @@ hook.target.path = new Set([
107112
'/api/cloudsearch/pc',
108113
'/api/v1/playlist/manipulate/tracks',
109114
'/api/song/like',
115+
'/api/radio/like',
110116
'/api/v1/play/record',
111117
'/api/playlist/v4/detail',
112118
'/api/v1/radio/get',
@@ -150,6 +156,22 @@ const domainList = [
150156
'interface3.music.163.com',
151157
];
152158

159+
/**
160+
* Set a request header, replacing whatever casing it already arrived under.
161+
* Node lowercases what it parses, but a header this hook added itself may not
162+
* be, and two spellings of the same name would both be sent.
163+
*
164+
* @param {Record<string, string>} headers
165+
* @param {string} name
166+
* @param {string} value
167+
*/
168+
const setHeader = (headers, name, value) => {
169+
const existing = Object.keys(headers).find(
170+
(key) => key.toLowerCase() === name.toLowerCase()
171+
);
172+
headers[existing || name.toLowerCase()] = value;
173+
};
174+
153175
/**
154176
* The `x-encr-sskey` a xeapi response carries is the very key the client will
155177
* encrypt its following requests with, and it arrives as a plain header — so
@@ -235,16 +257,42 @@ hook.request.before = (ctx) => {
235257
)
236258
ctx.decision = 'proxy';
237259

238-
if (process.env.NETEASE_COOKIE && url.path.includes('url')) {
239-
var cookies = cookieToMap(req.headers.cookie);
240-
var new_cookies = cookieToMap(process.env.NETEASE_COOKIE);
260+
if (url.path.includes('url')) {
261+
const replaced = [];
241262

242-
Object.entries(new_cookies).forEach(([key, value]) => {
243-
cookies[key] = value;
244-
});
263+
// Merge rather than replace: the client's own cookie carries the
264+
// platform and version this hook later reads back out of it.
265+
if (process.env.NETEASE_COOKIE) {
266+
const cookies = cookieToMap(req.headers.cookie || '');
267+
Object.entries(cookieToMap(process.env.NETEASE_COOKIE)).forEach(
268+
([key, value]) => {
269+
cookies[key] = value;
270+
}
271+
);
272+
setHeader(req.headers, 'Cookie', mapToCookie(cookies));
273+
replaced.push('Cookie');
274+
}
275+
276+
if (process.env.NETEASE_USER_AGENT) {
277+
setHeader(
278+
req.headers,
279+
'User-Agent',
280+
process.env.NETEASE_USER_AGENT
281+
);
282+
replaced.push('User-Agent');
283+
}
284+
285+
if (process.env.NETEASE_MCONFIG_INFO) {
286+
setHeader(
287+
req.headers,
288+
'MConfig-Info',
289+
process.env.NETEASE_MCONFIG_INFO
290+
);
291+
replaced.push('MConfig-Info');
292+
}
245293

246-
req.headers.cookie = mapToCookie(cookies);
247-
logger.debug('Replace netease cookie');
294+
if (replaced.length)
295+
logger.debug(`Replaced the netease ${replaced.join(', ')}.`);
248296
}
249297

250298
if (
@@ -494,7 +542,10 @@ hook.request.after = (ctx) => {
494542
) {
495543
if (netease.path.includes('manipulate'))
496544
return tryCollect(ctx);
497-
else if (netease.path === '/api/song/like')
545+
else if (
546+
netease.path === '/api/song/like' ||
547+
netease.path === '/api/radio/like'
548+
)
498549
return tryLike(ctx);
499550
} else if (netease.path.includes('url')) return tryMatch(ctx);
500551
else if (netease.path.includes('/usertool/sound/'))
@@ -942,14 +993,42 @@ const tryMatch = (ctx) => {
942993
let tasks;
943994
let target = 0;
944995

996+
// A refused response may carry nothing to patch — no entry, or none with a
997+
// usable id. Stand one in so the track still goes through the providers.
998+
if (BLOCKED_CODES.has(jsonBody.code)) {
999+
const present = Array.isArray(jsonBody.data)
1000+
? jsonBody.data[0]
1001+
: jsonBody.data;
1002+
const id =
1003+
(present && Number(present.id)) || requestedId(netease.param);
1004+
if (!id) {
1005+
logger.debug(
1006+
`The upstream refused with ${jsonBody.code} and named no song, so there is nothing to match.`
1007+
);
1008+
return;
1009+
}
1010+
logger.debug(
1011+
`The upstream refused with ${jsonBody.code}, falling back to the providers for ${id}.`
1012+
);
1013+
jsonBody.data = [
1014+
{ ...present, id, code: jsonBody.code, url: null, br: 0 },
1015+
];
1016+
}
1017+
9451018
const inject = (item) => {
1019+
// A blocked response can carry no entry at all.
1020+
if (!item) return;
9461021
item.flag = 0;
9471022
// A response to a download request may leave the id out, so fall back
9481023
// to the one that was asked for.
9491024
const songId = item.id || requestedId(netease.param);
9501025
if (
9511026
songId &&
952-
(item.code !== 200 || item.freeTrialInfo || item.br < min_br) &&
1027+
// A `200` that still has no url is as unplayable as an error.
1028+
(item.code !== 200 ||
1029+
!item.url ||
1030+
item.freeTrialInfo ||
1031+
item.br < min_br) &&
9531032
(target === 0 || songId === target)
9541033
) {
9551034
return match(songId)
@@ -1031,7 +1110,24 @@ const tryMatch = (ctx) => {
10311110
target = netease.web ? 0 : requestedId(netease.param); // reduce time cost
10321111
tasks = jsonBody.data.map((item) => inject(item));
10331112
}
1034-
return Promise.all(tasks).catch((e) => e && logger.error(e));
1113+
return Promise.all(tasks)
1114+
.then(() => {
1115+
// A response the upstream refused carries its verdict at the top
1116+
// level, and a client that reads that first never looks at the
1117+
// entry we just filled in. Once something is playable, the refusal
1118+
// no longer describes the response.
1119+
if (
1120+
BLOCKED_CODES.has(jsonBody.code) &&
1121+
Array.isArray(jsonBody.data) &&
1122+
jsonBody.data.some(
1123+
(item) => item && item.code === 200 && item.url
1124+
)
1125+
) {
1126+
jsonBody.code = 200;
1127+
delete jsonBody.message;
1128+
}
1129+
})
1130+
.catch((e) => e && logger.error(e));
10351131
};
10361132

10371133
/**

0 commit comments

Comments
 (0)