@@ -32,6 +32,11 @@ const PLAIN_ENDPOINT_OS = new Set(
3232 . filter ( Boolean )
3333) ;
3434const 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
3641const 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