├── .gitignore ├── CHANGELOG.md ├── README.md ├── sg_config.json ├── sign_copartner.py ├── sign_ninebot.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | __pycache__ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | `2025-03-07` 2 | 3 | ### 变更 4 | 5 | - 删除`什么值得买`脚本,推荐一个自用的[https://github.com/hex-ci/smzdm_script](https://github.com/hex-ci/smzdm_script) 6 | - 增加环境变量支持、自适应获取json文件位置。 7 | - 【音乐合伙人】 `旋律、演唱、歌词`评分支持 8 | - 【音乐合伙人】 乐评功能支持(开启会从一言api自动获取评论内容) 9 | - 【音乐合伙人】 额外评定数参数,可自行控制额外评定歌曲数量 10 | 11 | ### 提示 12 | 13 | 使用方式上有些许变化,请仔细阅读使用文档 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

qinglong-sign

2 | 3 |
4 | 基于青龙面板的脚本库 5 |
6 | 7 | ## ✨ 特性 8 | 9 | - 网易云音乐合伙人自动评分 ✔ 10 | - 九号出行APP自动签到 ✔ 11 | 12 | ## 🔨 使用 13 | 14 | 15 | #### 1.进入容器 16 | ``` 17 | docker exec -it ql bash 18 | ``` 19 | 修改`ql`为你的青龙容器名字 20 | 21 | #### 2.拉取仓库 22 | ``` 23 | ql repo https://github.com/KotoriMinami/qinglong-sign.git "sign_" "" "utils" 24 | ``` 25 | #### 3.配置脚本数据 26 | - 方式1 (使用json文件) 27 | 28 | 复制配置文件到青龙config目录下 29 | ``` 30 | cp /ql/repo/KotoriMinami_qinglong-sign/sg_config.json /ql/data/config/sg_check.json 31 | ``` 32 | 33 | 在青龙面板的配置目录(`/ql/data/config`)下找到 `sg_check.json` 文件 或 前往web端的`控制面板 / 配置文件` 下拉选择`sg_check.json`, 34 | 然后根据配置说明进行抓包配置即可。 35 | 36 | ## ⚙ 配置说明 37 | 38 | | 配置名称 | 参数说明 | 获取位置 | 39 | |:----------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------| 40 | | MUSIC_COPARTNER | "cookie":网易云音乐cookie;
"extra_count"(int类型): 额外评定歌曲数,无此参数默认为`7`;
"comment"(bool类型):评定歌曲同时是否留言,如开启会从一言api随机获取一句内容进行评论,无此参数默认为`false`
注意:启用评论后,会同步评论至歌曲下,这样才会获得积分,防社死默认关闭。 | [网易云音乐](https://music.163.com/) | 41 | | NINEBOT | "deviceId": "app中抓包/portal/api/user-sign/v1/sign(高版本app为/portal/api/user-sign/v2/sign)接口,从请求参数中获取"
"authorization": "抓包接口同上,从请求头中获取" | [九号出行APP](https://www.ninebot.com/app/) | 42 | 43 | - 方式2(配置环境变量) 44 | 45 | 前往web端的`控制面板 / 环境变量` 点击创建变量, 46 | 然后根据配置说明进行抓包配置即可。 47 | 48 | ## ⚙ 配置说明 49 | 50 | | 配置名称 | 格式 | 说明 | 获取位置 | 51 | |:--------------------|:--------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------| 52 | | MUSIC_COPARTNER_ENV | cookie#extra_count#comment | `cookie`、`extra_count`同上,`comment`为非0的值表示启用,默认不启用。
其中`extra_count、comment`可不填写,如填写需同时填写。
注意:启用评论后,会同步评论至歌曲下,这样才会获得积分,防社死默认关闭。
多个账号`&`分隔。
例:`cookie1&cookie2#7#1&cookie3#8#0 ` | [网易云音乐](https://music.163.com/) | 53 | | NINEBOT_ENV | deviceId#authorization | 同上,多个账号`&` 分隔。
例:`deviceId#authorization&deviceId#authorization` | [九号出行APP](https://www.ninebot.com/app/) | 54 | 55 | #### 4.安装下面两个依赖 56 | `PyExecJS` 57 | `pycryptodome` 58 | 59 | 不会的可以直接去 web端的`控制面板 / 依赖管理 / Python3` 处添加依赖 60 | 61 | 62 | 63 | ## 🔈 特别声明 64 | 65 | - 本仓库发布的脚本及其中涉及的任何解锁和解密分析脚本,仅用于测试和学习研究,禁止用于商业用途,不能保证其合法性,准确性,完整性和有效性,请根据情况自行判断。 66 | 67 | - 本项目内所有资源文件,禁止任何公众号、自媒体进行任何形式的转载、发布。 68 | 69 | - 本人对任何脚本问题概不负责,包括但不限于由任何脚本错误导致的任何损失或损害。 70 | 71 | - 间接使用脚本的任何用户,包括但不限于建立VPS或在某些行为违反国家/地区法律或相关法规的情况下进行传播, 本人对于由此引起的任何隐私泄漏或其他后果概不负责。 72 | 73 | - 请勿将本仓库的任何内容用于商业或非法目的,否则后果自负。 74 | 75 | - 如果任何单位或个人认为该项目的脚本可能涉嫌侵犯其权利,则应及时通知并提供身份证明,所有权证明,我们将在收到认证文件后删除相关脚本。 76 | 77 | - 任何以任何方式查看此项目的人或直接或间接使用该项目的任何脚本的使用者都应仔细阅读此声明。本人保留随时更改或补充此免责声明的权利。一旦使用并复制了任何相关脚本或Script项目的规则,则视为您已接受此免责声明。 78 | 79 | **您必须在下载后的24小时内从计算机或手机中完全删除以上内容** 80 | 81 | > ***您使用或者复制了本仓库且本人制作的任何脚本,则视为 `已接受` 此声明,请仔细阅读*** 82 | 83 | 84 | #### Tip: 刚学习玩这个, 大佬轻喷,后期可能不定期增加其它脚本 85 | 86 | ## 致谢 87 | 88 | [@Sitoi](https://github.com/Sitoi) 89 | 90 | [@yuxian158](https://github.com/yuxian158/) 91 | 92 | [@weixin_44530979](https://blog.csdn.net/weixin_44530979) 93 | -------------------------------------------------------------------------------- /sg_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "MUSIC_COPARTNER": [ 3 | { 4 | "cookie": "_ntes_nuid=xxx; _ns=xxx; _ntes_nnid=xxx; UM_distinctid=xxx; NMTID=xxx; WNMCID=xxx; WEVNSM=xxx; __csrf=xxx; MUSIC_U=xxx; ntes_kaola_ad=xxx; _iuqxldmzr_=xxx; JSESSIONID-WYYY=xxx;" 5 | }, 6 | { 7 | "cookie": "多账号 url 填写,请参考上面,url 以实际获取为准(遇到特殊字符如双引号\\\" 请加反斜杠转义)", 8 | "extra_count": 7, 9 | "commit": false 10 | } 11 | ], 12 | "NINEBOT": [ 13 | { 14 | "deviceId": "app中抓包/portal/api/user-sign/v1/sign接口,从请求参数中获取", 15 | "authorization": "抓包接口同上,从请求头中获取" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /sign_copartner.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | cron: 0 8 * * * 4 | new Env('网易音乐合伙人'); 5 | """ 6 | import random 7 | import requests 8 | import base64 9 | import codecs 10 | 11 | from Crypto.Cipher import AES 12 | from time import sleep 13 | from utils import GetConfig 14 | from notify import send 15 | 16 | 17 | def pkcs7padding(text): 18 | """ 19 | 实现PKCS#7填充算法 20 | 21 | Args: 22 | text: 需要填充的文本 23 | 24 | Returns: 25 | 填充后的文本 26 | """ 27 | bs = AES.block_size # 16 28 | length = len(text) 29 | bytes_length = len(bytes(text, encoding='utf-8')) 30 | padding_size = length if (bytes_length == length) else bytes_length 31 | padding = bs - padding_size % bs 32 | padding_text = chr(padding) * padding 33 | return text + padding_text 34 | 35 | 36 | def aes_encrypt(text, key, iv): 37 | """ 38 | AES加密函数 39 | 40 | Args: 41 | text: 待加密文本 42 | key: 加密密钥 43 | iv: 初始化向量 44 | 45 | Returns: 46 | 加密后的base64编码文本 47 | """ 48 | key_bytes = bytes(key, encoding='utf-8') 49 | _iv = bytes(iv, encoding='utf-8') 50 | cipher = AES.new(key_bytes, AES.MODE_CBC, _iv) 51 | # 处理明文 52 | content_padding = pkcs7padding(text) 53 | # 加密 54 | encrypt_bytes = cipher.encrypt(bytes(content_padding, encoding='utf-8')) 55 | # 重新编码 56 | encrypt_text = str(base64.b64encode(encrypt_bytes), encoding='utf-8') 57 | return encrypt_text 58 | 59 | 60 | def rsa_encrypt(text, pubKey, modulus): 61 | """ 62 | RSA加密函数 63 | 64 | Args: 65 | text: 待加密文本 66 | pubKey: 公钥 67 | modulus: 模数 68 | 69 | Returns: 70 | 加密后的十六进制文本 71 | """ 72 | text = text[::-1] 73 | rs = int(codecs.encode(text.encode('utf-8'), 'hex_codec'), 16) ** int(pubKey, 16) % int(modulus, 16) 74 | return format(rs, 'x').zfill(256) 75 | 76 | 77 | def get_one_text(): 78 | """ 79 | 获取一条关于网易云的一言 80 | 81 | Returns: 82 | 一条随机的一言文本 83 | """ 84 | url = "https://v1.hitokoto.cn/?c=j" 85 | res = requests.get(url).json() 86 | return res["hitokoto"] 87 | 88 | 89 | def parse_accounts(env_str): 90 | """ 91 | 解析账号配置字符串 92 | 93 | Args: 94 | env_str: 账号配置字符串,格式为cookie&extra_count#comment 95 | 96 | Returns: 97 | 解析后的账号配置列表 98 | """ 99 | accounts = env_str.split('&') 100 | result = [] 101 | for account in accounts: 102 | cookie, extra_count, comment = (account.split('#') + [None] * 3)[:3] 103 | result.append({ 104 | "cookie": cookie, "extra_count": extra_count, "comment": comment 105 | }) 106 | return result 107 | 108 | 109 | # 获取i值的函数,即随机生成长度为16的字符串 110 | def get_random_string(length): 111 | """ 112 | 生成指定长度的随机字符串 113 | 114 | Args: 115 | length: 要生成的字符串长度 116 | 117 | Returns: 118 | 指定长度的随机字符串 119 | """ 120 | chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 121 | result = "" 122 | for i in range(length): 123 | result += random.choice(chars) 124 | return result 125 | 126 | 127 | class Copartner(): 128 | """网易音乐合伙人签到类""" 129 | name = "音乐合伙人" 130 | 131 | def __init__(self, check_item): 132 | """ 133 | 初始化音乐合伙人签到类 134 | 135 | Args: 136 | check_item: 包含cookie、extra_count和comment的字典 137 | """ 138 | self.csrf = None 139 | self.musicDataUrl = "https://interface.music.163.com/api/music/partner/daily/task/get" 140 | self.userInfoUrl = "https://music.163.com/api/nuser/account/get" 141 | self.signUrl = "https://interface.music.163.com/weapi/music/partner/work/evaluate?csrf_token=" 142 | self.extraMusicDataUrl = "https://interface.music.163.com/api/music/partner/extra/wait/evaluate/work/list" 143 | self.reportListenUrl = 'https://interface.music.163.com/weapi/partner/resource/interact/report?csrf_token=' 144 | self.g = '0CoJUm6Qyw8W8jud' # buU9L(["爱心", "女孩", "惊恐", "大笑"])的值 145 | self.b = "010001" # buU9L(["流泪", "强"])的值 146 | # buU9L(Rg4k.md)的值 147 | self.c = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' 148 | self.i = get_random_string(16) # 随机生成长度为16的字符串 149 | self.iv = "0102030405060708" # 偏移量 150 | self.headers = { 151 | "Accept": "application/json, text/javascript", 152 | "Accept-Encoding": "gzip, deflate", 153 | "Accept-Language": "zh-CN,zh-Hans;q=0.9", 154 | "Content-Type": "application/x-www-form-urlencoded", 155 | "Origin": "http://mp.music.163.com", 156 | "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 CloudMusic/0.1.1 NeteaseMusic/8.8.01" 157 | } 158 | self.musicScoreRandomRange = [2, 4] 159 | self.waitRange = [15, 20] 160 | self.musicTags = [ 161 | [ 162 | "1-A-1", 163 | "1-B-1", 164 | "1-C-1", 165 | "1-D-1", 166 | "1-D-2" 167 | ], 168 | [ 169 | "2-A-1", 170 | "2-B-1", 171 | "2-C-1", 172 | "2-D-1", 173 | "2-D-2" 174 | ], 175 | [ 176 | "3-A-1", 177 | "3-A-2", 178 | "3-B-1", 179 | "3-C-1", 180 | "3-D-1", 181 | "3-D-2", 182 | "3-E-1", 183 | "3-E-2" 184 | ], 185 | [ 186 | "4-A-1", 187 | "4-A-2", 188 | "4-B-1", 189 | "4-C-1", 190 | "4-D-1", 191 | "4-D-2", 192 | "4-E-1", 193 | "4-E-2" 194 | ], 195 | [ 196 | "5-A-1", 197 | "5-A-2", 198 | "5-B-1", 199 | "5-C-1", 200 | "5-D-1", 201 | "5-D-2", 202 | "5-E-1", 203 | "5-E-2" 204 | 205 | ] 206 | ] 207 | self.cookie = check_item.get('cookie') 208 | self.extra_count = min(15, int(check_item.get('extra_count') or 7)) 209 | self.enable_comment = check_item.get('comment') is not None and check_item.get('comment') != '0' 210 | 211 | def get_enc_sec_key(self): 212 | """ 213 | 获取加密的SecKey 214 | 215 | Returns: 216 | 加密后的SecKey 217 | """ 218 | return rsa_encrypt(self.i, self.b, self.c) 219 | 220 | def get_params(self, data): 221 | """ 222 | 获取加密后的参数 223 | 224 | Args: 225 | data: 需要加密的数据 226 | 227 | Returns: 228 | 双重AES加密后的参数 229 | """ 230 | enc_text = str(data) 231 | return aes_encrypt(aes_encrypt(enc_text, self.g, self.iv), self.i, self.iv) 232 | 233 | def wait_listen(self): 234 | """ 235 | 模拟听歌等待时间 236 | """ 237 | wait = random.randint(self.waitRange[0], self.waitRange[1]) 238 | sleep(wait) 239 | 240 | def merge_comment_params(self, params): 241 | """ 242 | 合并评论参数 243 | 244 | Args: 245 | params: 原始参数字典 246 | 247 | Returns: 248 | 合并评论后的参数字典 249 | """ 250 | if self.enable_comment: 251 | params["comment"] = get_one_text() 252 | params["syncComment"] = "true" 253 | return params 254 | 255 | def sign(self, session, music_data, msg): 256 | """ 257 | 执行基础评定签到 258 | 259 | Args: 260 | session: 请求会话 261 | music_data: 音乐数据 262 | msg: 消息列表,用于记录签到结果 263 | """ 264 | works = music_data['works'] 265 | task_id = music_data['id'] 266 | begin = False 267 | for work in works: 268 | _work = work['work'] 269 | if work['completed']: 270 | msg.append({ 271 | "name": f"{_work['name']}({_work['authorName']})", 272 | "value": f"评分完成:{work['score']}分" 273 | }) 274 | else: 275 | if not begin: 276 | msg.append({"name": "本次进度", "value": ""}) 277 | begin = True 278 | self.wait_listen() # 等都等了,一起等吧.. 要是加了获取列表时间和提交时间,也能用 279 | score = self.get_random_score() 280 | tags = self.get_random_tags(score) 281 | params = self.merge_comment_params({ 282 | "taskId": task_id, 283 | "workId": _work['id'], 284 | "score": score, 285 | "tags": tags, 286 | "customTags": "[]", 287 | "comment": "", 288 | "syncYunCircle": "true", 289 | "syncComment": "false", 290 | "extraScore": str({ 291 | "1": self.get_random_score(), 292 | "2": self.get_random_score(), 293 | "3": self.get_random_score(), 294 | }), 295 | "source": "mp-music-partner", 296 | "csrf_token": self.csrf 297 | }) 298 | data = { 299 | "params": self.get_params(params).replace("\n", ""), 300 | "encSecKey": self.get_enc_sec_key() 301 | } 302 | try: 303 | response = session.post( 304 | url=f"{self.signUrl}={self.csrf}", 305 | data=data, headers=self.headers).json() 306 | if response["code"] == 200: 307 | msg.append({ 308 | "name": f"{_work['name']}({_work['authorName']})", 309 | "value": f"评分完成:{score}分" 310 | }) 311 | except Exception as e: 312 | print(f"歌曲 {_work['name']} 评分异常,原因{str(e)}") 313 | 314 | def sign_extra(self, session, music_data, task_id, msg): 315 | """ 316 | 执行额外评定签到 317 | 318 | Args: 319 | session: 请求会话 320 | music_data: 音乐数据 321 | task_id: 任务ID 322 | msg: 消息列表,用于记录签到结果 323 | """ 324 | work = music_data['work'] 325 | # 先上报,再提交 326 | report_data = { 327 | "params": self.get_params({ 328 | "workId": work['id'], 329 | "resourceId": work['resourceId'], 330 | "bizResourceId": "", 331 | "interactType": "PLAY_END" 332 | }), 333 | "encSecKey": self.get_enc_sec_key() 334 | } 335 | try: 336 | response = session.post( 337 | url=f"{self.reportListenUrl}={self.csrf}", 338 | data=report_data, headers=self.headers).json() 339 | if response["code"] != 200: 340 | msg.append({ 341 | "name": f"{work['name']}({work['authorName']})", 342 | "value": f"上报失败:{response['message']}" 343 | }) 344 | return 345 | except Exception as e: 346 | print(f"歌曲 {work['name']} 上报失败,原因{str(e)}") 347 | return 348 | 349 | # reportListen 350 | score = self.get_random_score() 351 | tags = self.get_random_tags(score) 352 | params = self.merge_comment_params({ 353 | "taskId": task_id, 354 | "workId": work['id'], 355 | "score": score, 356 | "tags": tags, 357 | "customTags": "[]", 358 | "comment": "", 359 | "syncYunCircle": "true", 360 | "syncComment": "false", 361 | "extraScore": str({ 362 | "1": self.get_random_score(), 363 | "2": self.get_random_score(), 364 | "3": self.get_random_score(), 365 | }), 366 | "extraResource": "true", 367 | "source": "mp-music-partner", 368 | "csrf_token": self.csrf 369 | }) 370 | 371 | data = { 372 | "params": self.get_params(params).replace("\n", ""), 373 | "encSecKey": self.get_enc_sec_key() 374 | } 375 | try: 376 | response = session.post( 377 | url=f"{self.signUrl}={self.csrf}", 378 | data=data, headers=self.headers).json() 379 | if response["code"] == 200: 380 | msg.append({ 381 | "name": f"{work['name']}({work['authorName']})", 382 | "value": f"评分完成:{score}分" 383 | }) 384 | else: 385 | msg.append({ 386 | "name": f"{work['name']}({work['authorName']})", 387 | "value": f"评分失败:{response['message']}" 388 | }) 389 | except Exception as e: 390 | print(f"歌曲 {work['name']} 评分异常,原因{str(e)}") 391 | 392 | def valid(self, session): 393 | """ 394 | 验证登录状态并获取音乐数据 395 | 396 | Args: 397 | session: 请求会话 398 | 399 | Returns: 400 | 成功返回(音乐数据, 用户名),失败返回(False, 错误信息) 401 | """ 402 | try: 403 | content = session.get(url=self.musicDataUrl, 404 | headers={**self.headers, "Referer": "https://mp.music.163.com/"}) 405 | except Exception as e: 406 | return False, f"登录验证异常,错误信息: {e}" 407 | data = content.json() 408 | if data["code"] == 301: 409 | return False, data["message"] 410 | if data["code"] == 200: 411 | music_data = data["data"] 412 | user_name = self.login_info(session=session)["profile"]["nickname"] 413 | return music_data, user_name 414 | return False, "登录信息异常" 415 | 416 | def get_random_score(self): 417 | """ 418 | 获取随机评分 419 | 420 | Returns: 421 | 随机评分值(2-4) 422 | """ 423 | return random.randint(self.musicScoreRandomRange[0], self.musicScoreRandomRange[1]) 424 | 425 | def get_random_tags(self, score): 426 | """ 427 | 根据评分获取随机标签 428 | 429 | Args: 430 | score: 评分值 431 | 432 | Returns: 433 | 随机选择的标签字符串,以逗号分隔 434 | """ 435 | num_to_select = random.randint(1, 3) 436 | current_score_tags = self.musicTags[score - 1] 437 | selected_values = random.sample(current_score_tags, num_to_select) 438 | return ','.join(selected_values) 439 | 440 | def get_extra_music(self, session): 441 | """ 442 | 获取额外评定音乐列表 443 | 444 | Args: 445 | session: 请求会话 446 | 447 | Returns: 448 | (未完成的额外评定列表, 已完成的额外评定数量) 449 | """ 450 | try: 451 | content = session.get(url=self.extraMusicDataUrl, 452 | headers={**self.headers, "Referer": "https://mp.music.163.com/"}) 453 | except Exception as e: 454 | return [], f"登录验证异常,错误信息: {e}" 455 | data = content.json() 456 | if data["code"] == 301: 457 | return False, data["message"] 458 | if data["code"] == 200: 459 | extra_music_data = data["data"] 460 | computed_music = [] 461 | undone_music = [] 462 | for x in extra_music_data: 463 | if x['completed']: 464 | computed_music.append(x) 465 | else: 466 | undone_music.append(x) 467 | extra_count = self.extra_count - len(computed_music) 468 | return [] if extra_count < 0 else undone_music[:extra_count], len(computed_music) 469 | return [], "登录信息异常" 470 | 471 | def login_info(self, session): 472 | """ 473 | 获取登录用户信息 474 | 475 | Args: 476 | session: 请求会话 477 | 478 | Returns: 479 | 用户信息字典 480 | """ 481 | try: 482 | return session.get(url=self.userInfoUrl, headers=self.headers).json() 483 | except Exception as e: 484 | print(e) 485 | return { 486 | "profile": { 487 | "nickname": "获取用户信息异常" 488 | } 489 | } 490 | 491 | def main(self): 492 | """ 493 | 主函数,执行签到流程 494 | 495 | Returns: 496 | 签到结果消息 497 | """ 498 | session = requests.session() 499 | cookie = self.cookie 500 | music_cookie = {item.split("=")[0]: item.split("=")[1] for item in cookie.split("; ")} 501 | self.csrf = music_cookie['__csrf'] 502 | requests.utils.add_dict_to_cookiejar(session.cookies, music_cookie) 503 | music_data, user_name = self.valid(session) 504 | if music_data: 505 | completed = music_data['completed'] 506 | msg = [ 507 | {"name": "帐号信息", "value": f"{user_name}"}, 508 | {"name": "当前进度", "value": ""}, 509 | {"name": "基础评定完成状态", "value": f"{'已完成' if completed else '未完成'}"}, 510 | {"name": "基础评定获得积分", "value": music_data['integral']}, 511 | {"name": "基础评定已完成数", "value": music_data["completedCount"]}, 512 | ] 513 | if not completed: 514 | self.sign(session, music_data, msg) 515 | extra_music_data, extra_music_computed_count = self.get_extra_music(session) 516 | if len(extra_music_data): 517 | msg.append({"name": "待额外评定数", "value": len(extra_music_data)}) 518 | task_id = music_data['id'] 519 | for x in extra_music_data: 520 | self.wait_listen() # 此接口单次提交,随机等待15-20s,假装在听歌? 521 | self.sign_extra(session, x, task_id, msg) 522 | else: 523 | msg.append({"name": "额外评定完成数", "value": f"{extra_music_computed_count}"}) 524 | else: 525 | msg = [ 526 | {"name": "帐号信息", "value": user_name}, 527 | {"name": "cookie信息", "value": "Cookie 可能过期"}, 528 | ] 529 | msg = "\n".join([f"{one.get('name')}: {one.get('value')}" for one in msg]) 530 | return msg 531 | 532 | 533 | @GetConfig(script_name='MUSIC_COPARTNER') 534 | def main(*args, **kwargs): 535 | """ 536 | 主入口函数 537 | 538 | Args: 539 | *args: 可变参数 540 | **kwargs: 关键字参数,包含accounts和accounts_env 541 | """ 542 | accounts = kwargs.get('accounts') 543 | accounts_env = kwargs.get('accounts_env') 544 | if accounts_env: 545 | accounts = parse_accounts(accounts_env) 546 | res = "" 547 | for index, account in enumerate(accounts): 548 | res = f'{res}账号{index + 1}:\n{Copartner(account).main()}\n' 549 | print(res) 550 | send('网易音乐合伙人', res) 551 | 552 | 553 | if __name__ == "__main__": 554 | main() 555 | -------------------------------------------------------------------------------- /sign_ninebot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | cron: 0 9 * * * 4 | new Env('九号出行'); 5 | """ 6 | 7 | import requests 8 | import json 9 | 10 | 11 | from notify import send 12 | from utils import GetConfig 13 | from datetime import datetime 14 | 15 | def parse_accounts(env_str): 16 | accounts = env_str.split('&') 17 | result = [] 18 | for account in accounts: 19 | device_id, authorization = account.split('#') 20 | result.append({ 21 | "deviceId": device_id, "authorization": authorization 22 | }) 23 | return result 24 | 25 | class Ninebot(): 26 | name = "九号出行" 27 | 28 | def __init__(self, check_item): 29 | self.signUrl = "https://cn-cbu-gateway.ninebot.com/portal/api/user-sign/v2/sign" 30 | self.validUrl = "https://cn-cbu-gateway.ninebot.com/portal/api/user-sign/v2/status" 31 | self.headers = { 32 | "Accept": "application/json, text/plain, */*", 33 | "Authorization": check_item.get("authorization"), 34 | "Accept-Encoding": "gzip, deflate, br", 35 | "Accept-Language": "zh-CN,zh-Hans;q=0.9", 36 | "Connection": "keep-alive", 37 | "Content-Type": "application/json", 38 | "Host": "cn-cbu-gateway.ninebot.com", 39 | "Origin": "https://h5-bj.ninebot.com", 40 | "from_platform_1": "1", 41 | "language": "zh", 42 | "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Segway v6 C 609033420", 43 | "Referer": "https://h5-bj.ninebot.com/" 44 | } 45 | self.check_item = check_item 46 | 47 | def sign(self, session, msg): 48 | try: 49 | response = session.post( 50 | url=self.signUrl, headers=self.headers, json={ 51 | "deviceId": self.check_item.get("deviceId") 52 | }) 53 | if response.status_code == 200: 54 | response_data = response.json() 55 | if (response_data.get("code") == 0): 56 | # checkin_num = response.json().get("data", {}).get('consecutiveDays') 57 | msg.append({"name": "签到成功", "value": f""}) 58 | else: 59 | msg.append({"name": "签到失败", "value": f"{response_data.get('msg')}"}) 60 | except Exception as e: 61 | msg.extend([ 62 | {"name": "签到信息", "value": "签到失败"}, 63 | {"name": "错误信息", "value": str(e)}, 64 | ]) 65 | 66 | def valid(self, session): 67 | try: 68 | content = session.get(url=f"{self.validUrl}?t={int(datetime.now().timestamp() * 1000)}", headers={**self.headers}) 69 | except Exception as e: 70 | return False, f"登录验证异常,错误信息: {e}" 71 | json_data = content.json() 72 | if content.status_code == 200: 73 | if json_data.get("code") == 0: 74 | return json_data.get("data", False), "" 75 | else: 76 | return False, json.get("msg") 77 | return False, "登录信息异常" 78 | 79 | def main(self): 80 | session = requests.session() 81 | valid_data, err_info = self.valid(session) 82 | if valid_data: 83 | completed = valid_data.get("currentSignStatus") == 1 84 | msg = [ 85 | {"name": "连续签到天数", "value": f"{valid_data.get('consecutiveDays', '')}天"}, 86 | {"name": "今日签到状态", "value": "已签到" if completed else "未签到"} 87 | ] 88 | if not completed: 89 | self.sign(session, msg) 90 | else: 91 | msg = [ 92 | {"name": "cookie信息", "value": f"{err_info}"}, 93 | ] 94 | msg = "\n".join([f"{one.get('name')}: {one.get('value')}" for one in msg]) 95 | return msg 96 | 97 | 98 | @GetConfig(script_name='NINEBOT') 99 | def main(*args, **kwargs): 100 | accounts = kwargs.get('accounts') 101 | accounts_env = kwargs.get('accounts_env') 102 | if accounts_env: 103 | accounts = parse_accounts(accounts_env) 104 | res = "" 105 | for index, account in enumerate(accounts): 106 | res = f'{res}账号{index + 1}:\n{Ninebot(account).main()}\n' 107 | print(res) 108 | send('九号出行', res) 109 | 110 | 111 | if __name__ == "__main__": 112 | main() 113 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from functools import wraps 5 | 6 | path_list = [ 7 | "/ql/config/sg_check.json", 8 | "/ql/data/config/sg_check.json", 9 | "sg_check.json", 10 | "./config/sg_check.json", 11 | "../config/sg_check.json", 12 | "sg_config.json", 13 | ] 14 | 15 | 16 | class GetConfig(object): 17 | def __init__(self, script_name): 18 | self.script_name = script_name 19 | 20 | def __call__(self, func): 21 | @wraps(func) 22 | def wrapper(*args, **kwargs): 23 | # 优先取环境变量 24 | configs_env = os.getenv(f'{self.script_name}_ENV') 25 | if configs_env is None: 26 | config_path = None 27 | for path in path_list: 28 | _config_path = os.path.join(os.getcwd(), path) 29 | if os.path.exists(_config_path): 30 | config_path = os.path.normpath(_config_path) 31 | break 32 | if config_path: 33 | print(f'配置地址:{config_path}') 34 | with open(config_path, encoding="utf-8") as f: 35 | try: 36 | json_data = json.loads(f.read()) 37 | accounts = json_data.get(self.script_name, []) 38 | func(*args, **kwargs, accounts = accounts) 39 | except Exception as e: 40 | print(e) 41 | print("获取JSON数据失败,请检查 sg_check.json 文件格式是否正确!") 42 | return; 43 | func(*args, **kwargs, accounts_env = configs_env) 44 | 45 | return wrapper --------------------------------------------------------------------------------