一直使用 Linux 工作、娱乐,因为 Linux 上的网易云音乐缓存在~/.cache/netease-cloud-music/CachedSongs
下。直接保存为 mp3 格式的。所以想下载一些会员才能下载的音乐,直接在网易云上听一遍,到缓存的位置找出来就行了。
最近表弟问我有没有网易云音乐的会员,我就在手机上找出网易云音乐的缓存文件,手动改了一个文件的后缀成mp3
, 好家伙,手机上的网易云音乐缓存竟然是加密的。emmm,开始 hack 网易云音乐缓存文件。
解密缓存文件
安卓手机上的缓存文件在netease/cloudmusic/Cache/Music1
里
缓存文件分为两种,一种是.idx!
,可以用文本编辑器直接打开,包含歌曲的id
信息;另一种是.uc!
,根据文件大小判断,很大可能是歌曲文件。
直接修改.uc!
文件后缀为.mp3
是没办法播放的,很多想从缓存文件中得到可播放文件的人已经不抱希望了。但是作为一个程序员,还是很希望知道怎么实现的。
上网找一圈,终于知道可以通过按位异或0xa3
得到的可播放文件。
另外直到现在才知道异或运输在二进制中是可逆运算。
网上有 java 和 python 的代码,但是没有 nodejs 的,于是自己动手写一个
1 2 3 4 5 6 7
| const fs = require("fs");
fs.readFile("./1498323856-96024-a1ccabb838649c9a8cc96ae5bd249a5e.mp3.uc!",(error, data) =>{ if (error) return ; const mp3 = data.map(value => value^0xA3); fs.writeFile("./1498323856-96024-a1ccabb838649c9a8cc96ae5bd249a5e.mp3", mp3,()=>{}); });
|
bingo,mp3 文件拿到了
获取歌曲信息
网易云缓存文件的命名实在是太奇怪了,一开始我还以为就是随机uuid
生成的命名,而.idx!
文件的内容改变了我的想法:
1 2 3 4 5 6 7 8 9 10 11 12 13
| { "duration": 266301, "filesize": 3247708, "musicId": 1498323856, "filemd5": "a1ccabb838649c9a8cc96ae5bd249a5e", "version": 2, "parts": [ "0,3247708" ], "bitrate": 96024, "md5": "4fbe5cc8a05fd998b560f342c86f6ec7" }
|
文件名被两个横杆-
分为三段:
- musicId: 音乐的 id
- bitrate: 比特率
- filemd5:
uc!
文件的 md5
我用上面的代码生成 mp3 文件后,生成 md5:
1 2 3 4
| ➜ md5 1498323856-96024-a1ccabb838649c9a8cc96ae5bd249a5e.mp3.uc\! 1498323856-96024-a1ccabb838649c9a8cc96ae5bd249a5e.mp3
A59B5C6B81B2FA2FCBA40CD256566B32 1498323856-96024-a1ccabb838649c9a8cc96ae5bd249a5e.mp3.uc! A1CCABB838649C9A8CC96AE5BD249A5E 1498323856-96024-a1ccabb838649c9a8cc96ae5bd249a5e.mp3
|
本来还想从.idx!
文件中回去人性化的文件名来着,看来这是没办法了。
我记得以前用 windows 的时候,有一些音乐文件是能看到图片的,而且属性里面还要很多信息。于是找一下怎么获取里面的数据。
原来 mp3 文件有metadata
,标签里面包含歌曲的歌手,歌曲图片,歌曲专辑等等信息。
找了一个现成的包,写了个 demo,才发现缓存文件里没有metadata
,但是代码对正常下载的网易云音乐文件还是能获取到信息的
1 2 3 4 5 6
| const NodeID3 = require('node-id3');
const fs = require('fs');
const tags = NodeID3.read("./3.mp3");
|
从文件获取id3
数据想法破灭了,那只能通过 api 来获取相关信息了:https://api.imjad.cn/cloudmusic/?type=detail&id=1498323856
也可以自己用 Binaryify/NeteaseCloudMusicApi 搭建一个 API。
nodejs 写一个脚本
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
|
const fs = require("fs"); const path = require("path");
let [,,input,output] = process.argv;
if (!output) { console.log("node insdex.js inputDirectory outputDirectory"); return; }
try { const stats = fs.statSync(input); if(!stats.isDirectory()) throw new Error(""); } catch (error) { return; }
const dir = fs.readdirSync(input).filter(value => path.extname(value) === ".uc!");
try { fs.mkdirSync(output); } catch (error) { }
Array.isArray(dir) && main(dir);
function main(dir) { const filename = path.join(input, dir.splice(0, 1)[0]); fs.readFile(filename, (error, data) => { if (error) return; const mp3 = data.map(buffer => buffer ^ 0xA3); const dlname = path.join(output, path.basename(filename, path.extname(filename))); fs.writeFile(dlname, mp3, () => { console.log(dir.length); dir.length && main(dir); }); }); }
|
用 go 写一个脚本工具
用我蹩脚的 go 写一个工具
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
|
package main
import ( "flag" "fmt" "io/ioutil" "os" "path" "strings" )
func uc2mp3(ucFilename string, mp3Filename string) {
buf, err := ioutil.ReadFile(ucFilename) if err != nil { fmt.Fprintf(os.Stderr, "File Error: %s\n", err) } arr := make([]uint8, len(buf)) for i, v := range buf { arr[i] = v ^ 0xA3 } err = ioutil.WriteFile(mp3Filename, arr, 0644) if err != nil { panic(err.Error()) } }
func main() { var dirName string var outputDir string var inputFilename string var rename string flag.StringVar(&dirName, "d", "", "输入文件夹") flag.StringVar(&outputDir, "o", "", "输出文件夹") flag.StringVar(&inputFilename, "f", "", "uc! 文件") flag.StringVar(&rename, "r", "", "mp3 重命名") flag.Parse() if len(flag.Args()) < 1 { flag.Usage() return } var ext string = ".uc!"
if inputFilename == "" && dirName != "" { stat, err := os.Stat(dirName) if err != nil { fmt.Fprintf(os.Stderr, "File Error: %s\n", err) panic(err.Error()) } if stat.IsDir() { dir, err := ioutil.ReadDir(dirName) if err != nil { fmt.Fprintf(os.Stderr, "File Error: %s\n", err) panic(err.Error()) }
for _, info := range dir { filename := info.Name()
if !info.IsDir() && path.Ext(filename) == ext { var outputFile string outputFile = strings.TrimSuffix(path.Base(filename), ext) uc2mp3(path.Join(dirName, filename), path.Join(outputDir, outputFile)) } } } } else if inputFilename != "" { if path.Ext(inputFilename) == ext { var outputFile string if rename != "" { outputFile = rename } else { outputFile = strings.TrimSuffix(path.Base(inputFilename), ext) } uc2mp3(inputFilename, path.Join(outputFile)) } } }
|
使用也很简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| uc2mp3 -h Usage of uc2mp3: -d string 输入文件夹 -f string uc! 文件 -o string 输出文件夹 -r string mp3 重命名
uc2mp3 -d /home/bubao/Downloads/Jami/music1 -o /home/bubao/Downloads/Jami/music
uc2mp3 -f /home/bubao/Downloads/Jami/music1/28136576-160000-14cca46fa243ecb590d3fcdf5e530cd1.mp3.uc\! -r "xx.mp3"
|
参考