@@ -122,15 +122,23 @@ hook.target.path = new Set([
122122 '/api/vipauth/app/auth/query' ,
123123 '/api/music-vip-membership/client/vip/info' ,
124124 '/api/music-vip-membership/front/vip/info' ,
125+ '/api/nuser/account/get' ,
126+ '/api/w/nuser/account/get' ,
125127] ) ;
126128
127- // The two interchangeable endpoints the client reads the membership state
128- // from. `client` is the one the app has always used; `front` is the newer
129- // variant, and either may show up standalone or inside a `/batch`.
129+ // The membership state reaches the client on two unrelated responses, and both
130+ // have to agree or the status looks unchanged: the entitlement endpoint says
131+ // what the account may play, while the account endpoint carries the flag the
132+ // badge is drawn from. Each has an older and a newer variant that are both
133+ // still served, and any of them may show up standalone or inside a `/batch`.
130134const VIP_INFO_PATHS = [
131135 '/api/music-vip-membership/client/vip/info' ,
132136 '/api/music-vip-membership/front/vip/info' ,
133137] ;
138+ const VIP_ACCOUNT_PATHS = [
139+ '/api/nuser/account/get' ,
140+ '/api/w/nuser/account/get' ,
141+ ] ;
134142
135143const domainList = [
136144 'music.163.com' ,
@@ -621,15 +629,56 @@ hook.negotiate.before = (ctx) => {
621629} ;
622630
623631/**
624- * Rewrite a membership response so the client believes it is a paying member.
625- * The state may arrive on its own or as one entry of a `/batch`, under either
626- * of the two interchangeable endpoints.
632+ * Rewrite the membership responses so the client believes it is a paying
633+ * member. This has to cover both signals: the entitlement endpoint the player
634+ * consults for playback, and the account endpoint the VIP badge is drawn from.
635+ * Either may arrive on its own or as one entry of a `/batch`.
627636 *
628637 * @param {{ path: string, jsonBody: Record<string, any> } } netease
629638 */
630639const applyLocalVip = ( netease ) => {
631640 const batched = netease . path === '/batch' || netease . path === '/api/batch' ;
632- if ( ! batched && ! VIP_INFO_PATHS . includes ( netease . path ) ) return ;
641+ const entry = ( path ) =>
642+ batched
643+ ? netease . jsonBody [ path ]
644+ : netease . path === path
645+ ? netease . jsonBody
646+ : undefined ;
647+ const store = ( path , value ) => {
648+ if ( batched ) netease . jsonBody [ path ] = value ;
649+ else netease . jsonBody = value ;
650+ } ;
651+
652+ VIP_INFO_PATHS . forEach ( ( path ) => {
653+ const info = entry ( path ) ;
654+ if ( info === undefined ) return ;
655+ if ( patchVipEntitlement ( info ) ) {
656+ store ( path , info ) ;
657+ logger . debug ( `Applied the local VIP on ${ path } .` ) ;
658+ }
659+ } ) ;
660+
661+ VIP_ACCOUNT_PATHS . forEach ( ( path ) => {
662+ const account = entry ( path ) ;
663+ if ( account === undefined ) return ;
664+ if ( patchVipAccount ( account ) ) {
665+ store ( path , account ) ;
666+ logger . debug ( `Applied the local VIP on ${ path } .` ) ;
667+ }
668+ } ) ;
669+ } ;
670+
671+ /**
672+ * The entitlement response: `associator`/`redplus`/`musicPackage` describe what
673+ * the account is allowed to play. `redplus` is the SVIP tier.
674+ *
675+ * @param {Record<string, any> } info
676+ * @return {boolean } Whether the response was patched.
677+ */
678+ const patchVipEntitlement = ( info ) => {
679+ if ( ! info || ! info . data ) return false ;
680+ if ( LOCAL_VIP_UID . length !== 0 && ! LOCAL_VIP_UID . includes ( info . data . userId ) )
681+ return false ;
633682
634683 const defaultPackage = {
635684 iconUrl : null ,
@@ -641,63 +690,73 @@ const applyLocalVip = (netease) => {
641690 } ;
642691 const vipLevel = 7 ; // ? months
643692
644- VIP_INFO_PATHS . forEach ( ( vipPath ) => {
645- if ( ! batched && netease . path !== vipPath ) return ;
646- const info = batched ? netease . jsonBody [ vipPath ] : netease . jsonBody ;
647- if ( ! info || ! info . data ) return ;
648- if (
649- LOCAL_VIP_UID . length !== 0 &&
650- ! LOCAL_VIP_UID . includes ( info . data . userId )
651- )
652- return ;
693+ try {
694+ const nowTime = info . data . now || new Date ( ) . getTime ( ) ;
695+ const expireTime = nowTime + 31622400000 ;
696+ info . data . redVipLevel = vipLevel ;
697+ info . data . redVipAnnualCount = 1 ;
653698
654- try {
655- const nowTime = info . data . now || new Date ( ) . getTime ( ) ;
656- const expireTime = nowTime + 31622400000 ;
657- info . data . redVipLevel = vipLevel ;
658- info . data . redVipAnnualCount = 1 ;
699+ info . data . musicPackage = {
700+ ...defaultPackage ,
701+ ...info . data . musicPackage ,
702+ vipCode : 230 ,
703+ vipLevel,
704+ expireTime,
705+ } ;
706+
707+ info . data . associator = {
708+ ...defaultPackage ,
709+ ...info . data . associator ,
710+ vipCode : 100 ,
711+ vipLevel,
712+ expireTime,
713+ } ;
659714
660- info . data . musicPackage = {
715+ if ( ENABLE_LOCAL_SVIP ) {
716+ info . data . redplus = {
661717 ...defaultPackage ,
662- ...info . data . musicPackage ,
663- vipCode : 230 ,
718+ ...info . data . redplus ,
719+ vipCode : 300 ,
664720 vipLevel,
665721 expireTime,
666722 } ;
667723
668- info . data . associator = {
724+ info . data . albumVip = {
669725 ...defaultPackage ,
670- ...info . data . associator ,
671- vipCode : 100 ,
672- vipLevel,
726+ ...info . data . albumVip ,
727+ vipCode : 400 ,
728+ vipLevel : 0 ,
673729 expireTime,
674730 } ;
731+ }
732+ return true ;
733+ } catch ( error ) {
734+ logger . debug ( { err : error } , 'Unable to apply the local VIP.' ) ;
735+ return false ;
736+ }
737+ } ;
675738
676- if ( ENABLE_LOCAL_SVIP ) {
677- info . data . redplus = {
678- ...defaultPackage ,
679- ...info . data . redplus ,
680- vipCode : 300 ,
681- vipLevel,
682- expireTime,
683- } ;
684-
685- info . data . albumVip = {
686- ...defaultPackage ,
687- ...info . data . albumVip ,
688- vipCode : 400 ,
689- vipLevel : 0 ,
690- expireTime,
691- } ;
692- }
739+ /**
740+ * The account response: `profile.vipType` is the flag the client draws the VIP
741+ * badge from. `11` is the ordinary black-vinyl VIP, `100` the SVIP tier. Left
742+ * untouched, the entitlement patch above shows nothing on the profile itself.
743+ *
744+ * @param {Record<string, any> } account
745+ * @return {boolean } Whether the response was patched.
746+ */
747+ const patchVipAccount = ( account ) => {
748+ const profile = account && account . profile ;
749+ if ( ! profile ) return false ;
750+ if ( LOCAL_VIP_UID . length !== 0 && ! LOCAL_VIP_UID . includes ( profile . userId ) )
751+ return false ;
693752
694- if ( batched ) netease . jsonBody [ vipPath ] = info ;
695- else netease . jsonBody = info ;
696- logger . debug ( `Applied the local VIP on ${ vipPath } .` ) ;
697- } catch ( error ) {
698- logger . debug ( { err : error } , 'Unable to apply the local VIP.' ) ;
699- }
700- } ) ;
753+ try {
754+ profile . vipType = ENABLE_LOCAL_SVIP ? 100 : 11 ;
755+ return true ;
756+ } catch ( error ) {
757+ logger . debug ( { err : error } , 'Unable to apply the local VIP account .' ) ;
758+ return false ;
759+ }
701760} ;
702761
703762const pretendPlay = ( ctx ) => {
0 commit comments