网易云音乐打卡脚本

网易云音乐打卡升级需要登录天数和听歌数。但是我是那种一首歌能单曲循环很久的人,这可怎么快速升级?那只能挂机了呗。

思路

1
登录->签到->听推荐歌->听 fm 推荐

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.
├── app.js # 可执行文件
├── .env # app.js 配置文件
├── .gitignore
├── index.js # 模块入口
├── LICENSE
├── module # 大部分代码来源自 https://github.com/Binaryify/NeteaseCloudMusicApi
│   ├── daily_signin.js
│   ├── level.js
│   ├── login_cellphone.js # 原 login
│   ├── login.js # 整和 login_cellphone 和 login_mail
│   ├── login_mail.js
│   ├── personal_fm.js
│   ├── playlist_detail.js
│   ├── recommend_resource.js
│   └── scrobble.js
├── package.json
├── README.md
└── util # 代码来源自 https://github.com/Binaryify/NeteaseCloudMusicApi
   ├── apicache.js
   ├── crypto.js
   ├── index.js
   ├── memory-cache.js
   └── request.js

代码

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// index.js
const request = require('./util/request')
const login = require('./module/login') // 登录
const level = require('./module/level')
const signin = require('./module/daily_signin') // 签到
const recommend_resource = require('./module/recommend_resource') // 获取用户所有的歌单列表
const playlist_detail = require('./module/playlist_detail') // 获取单个歌单的歌单内容
const scrobble = require('./module/scrobble') // 打卡
const personFm = require('./module/personal_fm')

const { cookieToJson } = require('./util/index')
const COOKIE = "os=pc;osver=Microsoft-Windows-10-Professional-build-10586-64bit;appver=2.0.3.131777;channel=netease;__remember_me=true"
class App {
constructor() {
this.getSongidIndex = 0;
}
/**
* @description 登录签到
* @author bubao
* @date 2020-09-09
* @param {string} username email or phone number
* @param {string} md5_password md5 password
* @memberof App
*/
async login(username, md5_password) {
const regexp = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
this.query = {
email: username,
md5_password,
cookie: cookieToJson(COOKIE)
}
const isMail = regexp.test(username);
if (!isMail) {
delete this.query.email;

if (username.length === 11) {
this.query.phone = username
} else {
[, this.query.phone] = username.split(2)
}
}

const result = await login(this.query, request)

if (result.status === 200) {
this.query.cookie = result.body.cookie
await signin(this.query, request).catch(err => err)
await signin({ ...this.query, type: 1 }, request).catch(err => err)
}

const levelData = await level(this.query, request)

this.config = {}
this.config.level = levelData.body.data.level; // 等级
this.config.nextPlayCount = levelData.body.data.nextPlayCount // 下一级需要播放的量
this.config.nowPlayCount = levelData.body.data.nowPlayCount // 现在播放的量
this.config.defaultPlayCount = this.config.nowPlayCount; // 启动时播放量
this.config.nowLoginCount = levelData.body.data.nowLoginCount // 现在登录数
this.config.nextLoginCount = levelData.body.data.nextLoginCount // 下一级需要登录数

this.config.diffPlayCount = this.config.nextPlayCount - this.config.nowPlayCount
this.config.diffLoginCount = this.config.nextLoginCount - this.config.nowLoginCount

this.config.diffNextLevelPlayCount = this.config.diffPlayCount < 300 ? 300 - this.config.diffPlayCount : 0
return
}

/**
* @description 获取歌单
* @author bubao
* @date 2020-07-05
* @returns {Promise<array>}
* @memberof App
*/
async recommend() {
const json = await recommend_resource(this.query, request)
return json.body.recommend.map(res => res.id) || []
}

async PersonFm() {
return (await personFm(this.query, request)).body.data;
}

async getSongid(playlist_id) {
if (playlist_id.length <= this.getSongidIndex) {
return []
}
const singleItem = playlist_id[this.getSongidIndex];
this.getSongidIndex++;
const result = await playlist_detail({ ...this.query, id: singleItem }, request);
return result.body.playlist.trackIds
}

async punch(songidlist, time, timer) {
if (songidlist.length === 0) {
return time;
}
while (songidlist.length) {
const singleSong = songidlist.splice(0, 1)[0]
try {
await scrobble({ ...this.query, sourceId: "", time: 240, id: singleSong.id }, request)
} catch (err) {
throw {
len: songidlist.length + 1
}
}
await timer()
}
const levelData = await level(this.query, request)
this.config.nowPlayCount = levelData.body.data.nowPlayCount
// console.log("nowPlayCount", this.config.nowPlayCount)
if (levelData.body.data.level === this.config.level) {
// console.log("StartPlay:", this.config.defaultPlayCount)
// console.log("nowPlayCount:", this.config.nowPlayCount)
const result = this.config.nowPlayCount - this.config.defaultPlayCount
// console.log("done: ", result)
return result
} else if (levelData.body.data.level === 10) {
return 300;
} else {
const result = this.config.diffNextLevelPlayCount + this.config.nowPlayCount + time
return result
}
}
}

process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
// application specific logging, throwing an error, or other logic here
});

module.exports = App
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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
// app.js

const App = require('.');
const fs = require('fs');
const util = require('util');
const fsRead = util.promisify(fs.readFile);
const fsWrite = util.promisify(fs.writeFile);

const app = new App();
const { timer, md5Password } = require("./util/index.js");

(async () => {

let config = {}
// 获取配置文件中的配置
config = JSON.parse(await fsRead(__dirname + '/.env').catch(async err => {
// 文件不存在,创建文件
config.username = '';
config.password = '';
await fsWrite(__dirname + '/.env', JSON.stringify(config));
return {};
}))

if (!config.username || config.username === '') {
console.log('add username and password in .env');
return
}

// * password2md5
if (config.password) {
config.md5_password = md5Password(config.password)
delete config.password;
fsWrite(__dirname + '/.env', JSON.stringify(config))
}

// 登录并签到
await app.login(config.username, config.md5_password);

// 获取播放列表
const playlistId = await app.recommend();

const TODAY = new Date().getDay();
// * 运行时间不为当天时间
if (config.runTime !== TODAY) {
config.isEnd = 0
} else {
config.isEnd = config.isEnd || 0
}

let isEnd = config.isEnd

if (!(config.runTime === TODAY && isEnd >= 300)) {
config = { ...config, ...app.config }
config.runTime = TODAY

if (config.err && (config.err.day !== config.runTime || config.getSongidStart !== config.runTime)) {
delete config.err
}
config.getSongidStart = config.runTime
console.log("config.err", config.err)
await fsWrite(__dirname + '/.env', JSON.stringify(config))
await timer(10)();
} else {
// 当运行时间为当天,并且 isEnd>=300 则完成。
console.log('It not work to do!!')
return
}

// times 私人 fm 循环次数
let times = 10000;

try {
// 当歌单结束标志不为今天并且 isEnd 标志小于 300
while (config.getSongidEnd !== TODAY && isEnd < 300) {
// 获取歌单列表
let songidlist = await app.getSongid(playlistId) || []
console.log("songidlist.length:", songidlist.length)
// 歌单长度为 0 则退出循环
if (songidlist.length === 0) {
break;
}
// 歌单执行时存在错误
if (config.err && config.err.len) {
// 为歌单当前 id
if (config.err.getSongidIndex === app.getSongidIndex) {
// 获取未播放的列表
songidlist = songidlist.slice(songidlist.length - config.len)
} else {
// 进入下一轮循环
continue;
}
}
// 执行打卡
isEnd = await app.punch(songidlist, isEnd, timer(3))
console.log(isEnd)
}
// * 歌单结束标志
config.getSongidEnd = TODAY;
// 歌单结束 任务未结束,则执行私人 fm 打卡
while (isEnd < 300 && times > 0) {
const songidlist = await app.PersonFm()
console.log("songidlist:", songidlist)
isEnd = await app.punch(songidlist, isEnd, timer(3));
times--;
console.log(isEnd);
}

config.isEnd = isEnd;
config.yesterday = config.nowPlayCount
} catch (error) {
config.isEnd = isEnd - 1;
config.err = {
day: TODAY,
getSongidIndex: app.getSongidIndex
}
if (error.len !== undefined) {
config.err.len = error.len
}
console.error(error)
}

await fsWrite(__dirname + '/.env', JSON.stringify(config)).catch(console.log)
console.log("It was done!")
})()

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
// module/login.js
// 整合登录
const login_mail = require('./login_mail')
const login_cellphone = require('./login_cellphone')

/**
*
* @param {{
login: String,
countrycode: String,
password: String,
cookie: String,
proxy?: String
}} query 参数
* @param {Request} request 请求方法
* @returns {Promise<{
status: 200,
cookie: String,
body: {
cookie: String
any,
}
}
>}
*/
function login(query, request) {
const isMail = !!query.email

return isMail ? login_mail(query, request) : login_cellphone(query, request);
}

module.exports = login;
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
// module/login_cellphone.js
// 手机登录

const crypto = require('crypto')

module.exports = async (query, request) => {
query.cookie.os = 'pc'
const data = {
phone: query.phone,
countrycode: query.countrycode,
password: query.md5_password || crypto.createHash('md5').update(query.password).digest('hex'),
rememberLogin: 'true'
}
let result = await request(
'POST', `https://music.163.com/weapi/login/cellphone`, data,
{ crypto: 'weapi', ua: 'pc', cookie: query.cookie, proxy: query.proxy }
)

if (result.body.code === 200) {
result = {
status: 200,
body: {
...result.body,
cookie: result.cookie.join(';')
},
cookie: result.cookie
}
}
return result
}

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
// module/login_mail.js
// 邮箱登录

const crypto = require('crypto')

module.exports = async (query, request) => {
query.cookie.os = 'pc'
const data = {
username: query.email,
password: query.md5_password || crypto.createHash('md5').update(query.password).digest('hex'),
rememberLogin: 'true'
}
let result = await request(
'POST', `https://music.163.com/weapi/login`, data,
{ crypto: 'weapi', ua: 'pc', cookie: query.cookie, proxy: query.proxy }
)
if (result.body.code === 502) {
return {
status: 200,
body: {
'msg': '账号或密码错误',
'code': 502,
'message': '账号或密码错误'
}
}
}
if (result.body.code === 200) {
result = {
status: 200,
body: {
...result.body,
cookie: result.cookie.join(';')
},
cookie: result.cookie
}
}
return result
}
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
// util/index.js
const crypto = require('crypto')

module.exports = {
toBoolean(val) {
if (val === '') return val
return val === 'true' || val == '1'
},
cookieToJson(cookie) {
if (!cookie) return {}
let cookieArr = cookie.split(';');
let obj = {}
cookieArr.forEach((i) => {
let arr = i.split('=');
obj[arr[0]] = arr[1];
});
return obj
},
timer(t = 1) {
return (() => new Promise((resolve) => {
setTimeout(resolve, t * 1000)
}))
},
md5Password(password) {
return crypto.createHash('md5').update(password).digest('hex');
}
}

使用

在项目的根目录下创建一个.env文件,添加下面的内容

1
2
3
4
{
"username": "phonenumber",
"password": "1234"
}

安装依赖

1
npm i request pac-proxy-agent

运行

1
node app.js

网易云音乐打卡脚本
https://bubao.github.io/posts/8d1e2c79.html
作者
一念
发布于
2020年10月18日
许可协议