├── unlock.dylib ├── unlock_outside.dylib ├── NeteaseMusic ├── unlock.sh ├── README.md └── hijack.m /unlock.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YiuChoi/163music-mac-client-unlock/master/unlock.dylib -------------------------------------------------------------------------------- /unlock_outside.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YiuChoi/163music-mac-client-unlock/master/unlock_outside.dylib -------------------------------------------------------------------------------- /NeteaseMusic: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | export DYLD_INSERT_LIBRARIES=/Applications/NeteaseMusic.app/Contents/MacOS/unlock.dylib 3 | exec /Applications/NeteaseMusic.app/Contents/MacOS/NeteaseMusic.org 4 | -------------------------------------------------------------------------------- /unlock.sh: -------------------------------------------------------------------------------- 1 | OUTSIDE="inside" 2 | if [ -z "$1" ] 3 | then 4 | OUTSIDE=$1 5 | fi 6 | 7 | mv /Applications/NeteaseMusic.app/Contents/MacOS/NeteaseMusic /Applications/NeteaseMusic.app/Contents/MacOS/NeteaseMusic.org 8 | cp NeteaseMusic /Applications/NeteaseMusic.app/Contents/MacOS/ 9 | UNLOCKFN="unlock.dylib" 10 | if [ "$OUTSIDE" = "outside" ] 11 | then 12 | UNLOCKFN="unlock_outside.dylib" 13 | fi 14 | cp $UNLOCKFN /Applications/NeteaseMusic.app/Contents/MacOS/unlock.dylib 15 | 16 | chmod +x /Applications/NeteaseMusic.app/Contents/MacOS/NeteaseMusic 17 | 18 | if [ -z "$2" ] 19 | then 20 | if [ "$2" = "true" ] 21 | then 22 | open -a NeteaseMusic 23 | fi 24 | fi 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 163music-mac-client-unlock 2 | Unlock netease music mac client using dylib inject 3 | 4 | # Usage 5 | 6 | 7 | ## Auto Unlock with Script 8 | 9 | We currently support auto unlock using following commands. 10 | 目前我们支持使用如下指令脚本自动安装。 11 | 12 | sh unlock.sh [inside|outside] [false|true] 13 | 14 | ### Inside China 15 | 你可以直接运行: 16 | sh unlock.sh 17 | 并重新打开App. 18 | 19 | 如果你想脚本解锁后自动帮你打开,请运行 20 | sh unlock inside true. 21 | 22 | ### Outside China 23 | 请运行: 24 | sh unlock.sh outside 25 | 并重新打开App. 26 | 27 | 如果你想脚本解锁后自动帮你打开,请运行 28 | sh unlock.sh outside true 29 | 30 | 31 | ## Manual Unlock 32 | 33 | ### Inside China 34 | 1. Download this repo 35 | 2. Enter /Applications/NeteaseMusic.app/Contents/MacOS 36 | 3. Rename "NeteaseMusic" to "NeteaseMusic.org" 37 | 4. Put unlock.dylib and NeteaseMusic to the folder 38 | 5. Enjoy! All paid and blocked music are unlocked ! 39 | 40 | ### Outside China 41 | 1. Download this repo 42 | 2. Enter /Applications/NeteaseMusic.app/Contents/MacOS 43 | 3. Rename "NeteaseMusic" to "NeteaseMusic.org" 44 | 4. Put unlock_outside.dylib and NeteaseMusic to the folder 45 | 5. Rename unlock_outside.dylib to unlock.dylib 46 | 5. Enjoy! All paid and blocked music are unlocked ! 47 | 48 | 49 | If you got LSOpenURL error , run ```chmod +x /Applications/NeteaseMusic.app/Contents/MacOS/NeteaseMusic``` 50 | 51 | # UPDATE:情况说明 52 | 本 dylib 可以用于解锁部分付费歌曲,以及大部分被下架/版权的歌曲。 53 | 54 | 但是对于部分已经下架的日语歌,网易最近直接在 CDN 上把文件给删掉了,访问全部 404,如果国内版不行,可以试试海外版,可能还存在一些缓存,如果有缓存赶紧下载,如果下不到那就没办法了,过段时间想想能不能利用音乐云盘绕过。 55 | 56 | 57 | # App Store 58 | Apps from app store will apply force signature verification, will not work after replace the main executeable, if you are using app store version , please use offical website version instead. 59 | 60 | # Build 61 | 62 | If you need change the source code , build with this command 63 | 64 | ```clang -framework Foundation -o unlock.dylib -dynamiclib hijack.m``` 65 | -------------------------------------------------------------------------------- /hijack.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | NSMutableDictionary *MusicIDsMap; 10 | 11 | @interface HijackURLProtocol : NSURLProtocol 12 | @property(nonatomic, strong) NSURLConnection *connection; 13 | @property(nonatomic, strong) NSMutableData *responseData; 14 | @end 15 | 16 | @implementation HijackURLProtocol{ 17 | BOOL isStream; 18 | } 19 | 20 | + (BOOL)canInitWithRequest:(NSURLRequest *)request { 21 | if ([NSURLProtocol propertyForKey:@"Hijacked" inRequest:request]) { 22 | return NO; 23 | }else if ([[[request URL] path] isEqualToString:@"/eapi/v3/song/detail"]) { 24 | return YES; 25 | }else if([[[request URL] path] isEqualToString:@"/eapi/v3/playlist/detail"]){ 26 | return YES; 27 | }else if([[[request URL] path] containsString:@"/eapi/song/enhance/player/url"]){ 28 | return YES; 29 | }else if([[[request URL] path] containsString:@"/eapi/cloudsearch/pc"]){ 30 | return YES; 31 | }else if([[[request URL] path] containsString:@"/eapi/v1/album"]){ 32 | return YES; 33 | }else if([[[request URL] path] containsString:@"/eapi/v1/artist"]){ 34 | return YES; 35 | }else if([[[request URL] host] isEqualToString:@"p2.music.126.net"]){ 36 | return YES; 37 | } 38 | return NO; 39 | } 40 | 41 | + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { 42 | return request; 43 | } 44 | 45 | + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b { 46 | return [super requestIsCacheEquivalent:a toRequest:b]; 47 | } 48 | 49 | - (void)startLoading { 50 | if([[[self.request URL] host] isEqualToString:@"p2.music.126.net"]){ 51 | NSLog(@"Using stream mode"); 52 | isStream = YES; 53 | } 54 | NSMutableURLRequest *newRequest = [self.request mutableCopy]; 55 | [NSURLProtocol setProperty:@YES forKey:@"Hijacked" inRequest:newRequest]; 56 | self.connection = [NSURLConnection connectionWithRequest:newRequest delegate:self]; 57 | } 58 | 59 | - (void)stopLoading { 60 | [self.connection cancel]; 61 | self.connection = nil; 62 | } 63 | 64 | - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { 65 | self.responseData = [[NSMutableData alloc] init]; 66 | NSURLResponse *res; 67 | if(isStream){ 68 | NSHTTPURLResponse *httpres = (NSHTTPURLResponse *)response; 69 | NSMutableDictionary *httpResponseHeaderFields = [[httpres allHeaderFields] mutableCopy]; 70 | httpResponseHeaderFields[@"Content-Type"] = @"audio/mpeg"; 71 | res = [[NSHTTPURLResponse alloc] initWithURL:self.request.URL 72 | statusCode:200 73 | HTTPVersion:@"1.1" 74 | headerFields:httpResponseHeaderFields]; 75 | NSLog(@"Fix mime-type to audio/mpeg, %@",res); 76 | }else{ 77 | res = response; 78 | } 79 | [self.client URLProtocol:self 80 | didReceiveResponse:res 81 | cacheStoragePolicy:NSURLCacheStorageNotAllowed]; 82 | } 83 | 84 | - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { 85 | if(isStream){ 86 | [self.client URLProtocol:self didLoadData:data]; 87 | }else{ 88 | [self.responseData appendData:data]; 89 | } 90 | } 91 | 92 | - (void)connectionDidFinishLoading:(NSURLConnection *)connection { 93 | if([[[self.request URL] path] containsString:@"/eapi/song/enhance/player/url"]){ 94 | return [self getEnhancePlayURL]; 95 | }else if(isStream){ 96 | return [self.client URLProtocolDidFinishLoading:self]; 97 | } 98 | id res = [NSJSONSerialization JSONObjectWithData:self.responseData options:NSJSONReadingMutableContainers error:nil]; 99 | if(!res || ![res count]){ 100 | NSLog(@"Cannot get json data"); 101 | return [self returnOriginData]; 102 | } 103 | 104 | int replaced = 0; 105 | 106 | // Privileges 107 | if(![self isEmpty:res[@"privileges"]]){ 108 | int count = [res[@"privileges"] count]; 109 | for (int i = 0; i < count; i++) { 110 | NSNumber *st = res[@"privileges"][i][@"st"]; 111 | NSNumber *fee = res[@"privileges"][i][@"fee"]; 112 | if(st.intValue < 0 || fee.intValue > 0){ 113 | res[@"privileges"][i] = [self replacePrivilege:res[@"privileges"][i]]; 114 | replaced++; 115 | } 116 | } 117 | } 118 | 119 | // Search Results 120 | if(![self isEmpty:res[@"result"]] && ![self isEmpty:res[@"result"][@"songs"]]){ 121 | int scount = [res[@"result"][@"songs"] count]; 122 | for (int i = 0; i < scount; i++) { 123 | id song = res[@"result"][@"songs"][i]; 124 | song[@"st"] = @0; 125 | song[@"fee"] = @0; 126 | song[@"privilege"] = [self replacePrivilege:song[@"privilege"]]; 127 | replaced++; 128 | MusicIDsMap[song[@"id"]] = song; 129 | res[@"result"][@"songs"][i] = song; 130 | } 131 | } 132 | 133 | 134 | // Songs 135 | if(![self isEmpty:res[@"songs"]]){ 136 | int scount = [res[@"songs"] count]; 137 | for (int i = 0; i < scount; i++) { 138 | id song = res[@"songs"][i]; 139 | song[@"st"] = @0; 140 | song[@"fee"] = @0; 141 | if(![self isEmpty:song[@"privilege"]]){ 142 | song[@"privilege"] = [self replacePrivilege:song[@"privilege"]]; 143 | replaced++; 144 | } 145 | MusicIDsMap[song[@"id"]] = song; 146 | res[@"songs"][i] = song; 147 | } 148 | } 149 | 150 | // Hot Songs 151 | if(![self isEmpty:res[@"hotSongs"]]){ 152 | int scount = [res[@"hotSongs"] count]; 153 | for (int i = 0; i < scount; i++) { 154 | id song = res[@"hotSongs"][i]; 155 | song[@"st"] = @0; 156 | song[@"fee"] = @0; 157 | if(![self isEmpty:song[@"privilege"]]){ 158 | song[@"privilege"] = [self replacePrivilege:song[@"privilege"]]; 159 | replaced++; 160 | } 161 | MusicIDsMap[song[@"id"]] = song; 162 | res[@"hotSongs"][i] = song; 163 | } 164 | } 165 | 166 | // Playlists 167 | if(![self isEmpty:res[@"playlist"]] && ![self isEmpty:res[@"playlist"][@"tracks"]]){ 168 | int scount = [res[@"playlist"][@"tracks"] count]; 169 | for (int i = 0; i < scount; i++) { 170 | res[@"playlist"][@"tracks"][i][@"st"] = @0; 171 | res[@"playlist"][@"tracks"][i][@"fee"] = @0; 172 | MusicIDsMap[res[@"playlist"][@"tracks"][i][@"id"]] = res[@"playlist"][@"tracks"][i]; 173 | } 174 | } 175 | 176 | NSLog(@"蛤蛤!替换了 %d 首被下架的歌曲",replaced); 177 | NSData *d = [NSJSONSerialization dataWithJSONObject:res options:0 error:nil]; 178 | [self.client URLProtocol:self didLoadData:d]; 179 | [self.client URLProtocolDidFinishLoading:self]; 180 | } 181 | 182 | - (NSDictionary *)replacePrivilege:(NSDictionary *)dict{ 183 | NSMutableDictionary *res = [dict mutableCopy]; 184 | res[@"st"] = @0; 185 | res[@"pl"] = res[@"maxbr"]; 186 | res[@"dl"] = res[@"maxbr"]; 187 | res[@"fl"] = res[@"maxbr"]; 188 | res[@"sp"] = @7; 189 | res[@"cp"] = @1; 190 | res[@"subp"] = @1; 191 | res[@"fee"] = @0; 192 | return res; 193 | } 194 | 195 | - (void)returnOriginData{ 196 | [self.client URLProtocol:self didLoadData:self.responseData]; 197 | [self.client URLProtocolDidFinishLoading:self]; 198 | } 199 | 200 | - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { 201 | [self.client URLProtocol:self didFailWithError:error]; 202 | } 203 | 204 | - (void)getEnhancePlayURL{ 205 | id res = [NSJSONSerialization JSONObjectWithData:self.responseData options:NSJSONReadingMutableContainers error:nil]; 206 | if(!res || ![res count]){ 207 | NSLog(@"Cannot get json data"); 208 | return [self returnOriginData]; 209 | } 210 | int count = [res[@"data"] count]; 211 | int replaced = 0; 212 | for (int i = 0; i < count; i++) { 213 | NSString *url = res[@"data"][i][@"url"]; 214 | if (!url || [url isEqual:[NSNull null]]){ 215 | res[@"data"][i][@"code"] = @200; 216 | res[@"data"][i][@"br"] = @320000; 217 | res[@"data"][i][@"url"] = [self combCDNPlayURL:res[@"data"][i][@"id"]]; 218 | replaced++; 219 | } 220 | } 221 | NSLog(@"Excited! 拼接了 %d 个 URL",replaced); 222 | NSData *d = [NSJSONSerialization dataWithJSONObject:res options:0 error:nil]; 223 | [self.client URLProtocol:self didLoadData:d]; 224 | [self.client URLProtocolDidFinishLoading:self]; 225 | } 226 | 227 | - (NSNumber *)selectFid:(NSNumber *)mid{ 228 | NSString *requrl = [NSString stringWithFormat:@"http://music.163.com/api/song/detail/?id=%@&ids=[%@]",mid,mid]; 229 | NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:requrl]]; 230 | request.HTTPMethod = @"GET"; 231 | request.timeoutInterval = 5; 232 | [request setValue:@"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/601.5.17 (KHTML, like Gecko)" forHTTPHeaderField:@"User-Agent"]; 233 | 234 | NSURLResponse * response = nil; 235 | NSError * error = nil; 236 | NSData * returnData = [NSURLConnection sendSynchronousRequest:request 237 | returningResponse:&response 238 | error:&error]; 239 | if(!returnData){ 240 | return nil; 241 | } 242 | 243 | id res = [NSJSONSerialization JSONObjectWithData:returnData options:0 error:nil]; 244 | if(!res || ![res count]){ 245 | NSLog(@"Cannot get json data"); 246 | return nil; 247 | } 248 | NSNumber *fid; 249 | id dic = res[@"songs"][0]; 250 | if(![self isEmpty:dic[@"hMusic"]]){ 251 | fid = dic[@"hMusic"][@"dfsId"]; 252 | }else if(![self isEmpty:dic[@"mMusic"]]){ 253 | fid = dic[@"mMusic"][@"dfsId"]; 254 | }else if(![self isEmpty:dic[@"bMusic"]]){ 255 | fid = dic[@"bMusic"][@"dfsId"]; 256 | }else if(![self isEmpty:dic[@"audition"]]){ 257 | fid = dic[@"audition"][@"dfsId"]; 258 | } 259 | return fid; 260 | } 261 | 262 | - (NSString *)combCDNPlayURL:(NSNumber *)mid{ 263 | NSNumber *fid; 264 | NSDictionary *dic = MusicIDsMap[mid]; 265 | if (!dic || [dic isEqual:[NSNull null]] || ![dic count]){ 266 | fid = [self selectFid:mid]; 267 | }else{ 268 | NSLog(@"Selecting fid: %@",dic); 269 | if(![self isEmpty:dic[@"h"]]){ 270 | fid = dic[@"h"][@"fid"]; 271 | }else if(![self isEmpty:dic[@"m"]]){ 272 | fid = dic[@"m"][@"fid"]; 273 | }else if(![self isEmpty:dic[@"l"]]){ 274 | fid = dic[@"l"][@"fid"]; 275 | }else if(![self isEmpty:dic[@"a"]]){ 276 | fid = dic[@"a"][@"fid"]; 277 | } 278 | NSLog(@"Fid selected"); 279 | } 280 | if(!fid){ 281 | NSLog(@"Fid select failed"); 282 | return nil; 283 | } 284 | NSString *fidstr = [NSString stringWithFormat:@"%@",fid]; 285 | const char *c = "3go8&$8*3*3h0k(2)2"; 286 | const char *f = [fidstr UTF8String]; 287 | int fidlen = strlen(f); 288 | char result[fidlen+1]; 289 | for (int i = 0; i < fidlen; i++){ 290 | result[i] = f[i] ^ c[i % 18]; 291 | } 292 | result[fidlen] = '\0'; 293 | 294 | unsigned char digest[CC_MD5_DIGEST_LENGTH]; 295 | CC_MD5(result, fidlen, digest); 296 | NSData *hashData = [[NSData alloc] initWithBytes:digest length: sizeof digest]; 297 | 298 | NSString *base64String = [hashData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; 299 | base64String = [base64String stringByReplacingOccurrencesOfString:@"/" withString:@"_"]; 300 | base64String = [base64String stringByReplacingOccurrencesOfString:@"+" withString:@"-"]; 301 | #ifndef OUTSIDE_CHINA 302 | NSString *finalURL = [NSString stringWithFormat:@"http://m%d.music.126.net/%@/%@.mp3",arc4random_uniform(2)+1,base64String,fid]; 303 | #else 304 | NSString *finalURL = [NSString stringWithFormat:@"http://p2.music.126.net/%@/%@.mp3",base64String,fid]; 305 | #endif 306 | NSLog(@"FinalURL: %@", finalURL); 307 | return finalURL; 308 | } 309 | 310 | - (BOOL)isEmpty:(id)obj{ 311 | if(!obj || [obj isEqual:[NSNull null]] || ![obj count]){ 312 | return YES; 313 | } 314 | return NO; 315 | } 316 | 317 | @end 318 | 319 | BOOL isLoaded = NO; 320 | 321 | __attribute__((constructor)) void DllMain() 322 | { 323 | if (!isLoaded) { 324 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ 325 | MusicIDsMap = [[NSMutableDictionary alloc] init]; 326 | if ([NSURLProtocol registerClass:[HijackURLProtocol class]]) { 327 | NSLog(@"[NMUnlock] 插♂入成功! "); 328 | } else { 329 | NSLog(@"[NMUnlock] 我去竟然失败了"); 330 | } 331 | isLoaded = YES; 332 | }); 333 | } 334 | } 335 | --------------------------------------------------------------------------------