#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ unlockmusic.py —— 主脚本:扫码登录 → 解锁(解禁)歌曲 → 登出 ================================================================================ **它没有自己的协议实现** —— 整个流程直接复用 `upsert.py` 的 `run()`: 扫码 → 预览 → 登录 → UpsertUserAll → 落库回查 → 登出,一模一样。 唯一多出来的东西是"要解锁哪些歌"这个入参。 -------------------------------------------------------------------------------- 为什么解锁不用单独的接口 -------------------------------------------------------------------------------- 游戏里解锁不是一次独立的网络请求,而是**往 userItemList 里补几条 UserItem**: itemKind = 5 歌曲本体解锁(MusicUnlockList) itemKind = 6 Master 难度解锁(MusicUnlockMasterList) itemKind = 7 Re:Master 难度解锁(MusicUnlockReMasterList) 依据 source/Assembly-CSharp/Net.VO.Mai2/VOExtensions.cs:981-1008 (UserData.cs:132-138 那四个解锁列表不是独立节点,是并进 userItemList 的)。 条目长这样: {"itemKind": 5, "itemId": 11414, "stock": 1, "isValid": true} ★ 标准谱(musicId < 10000)的 Master / Re:Master **天生解锁**,不会写 kind6/7 (source/Assembly-CSharp/Manager/NotesListManager.cs:415/455 开头就是 `if (id < 10000) return true;`);只有 DX 谱(>= 10000)才需要额外解禁。 ★ 服务器上已经有的条目**自动跳过** —— 重复插入会让服务端直接 HTTP 500。 ★ Re:Master 还受活动开关约束(IsUnlockReMaster 里的 IsOpenEvent), 那是机台侧开关,脚本改不了;写了也可能进游戏仍是灰的。 ★ 本功能**没有真机抓包可对照**(机台侧的解锁不上报),是按源码实现的。 -------------------------------------------------------------------------------- 用法 -------------------------------------------------------------------------------- python unlockmusic.py <二维码> --music 11414 # 预检,不发包 python unlockmusic.py <二维码> --music 11414 --send # ★ 真实解锁 python unlockmusic.py <二维码> --music 1085,11414 --send # 多首,逗号分隔 python unlockmusic.py <二维码> --music 11414 --send --no-unlock-remaster # 只要歌曲本体,不解禁任何难度 python unlockmusic.py <二维码> --music 11414 --send --no-unlock-master --no-unlock-remaster # 和其它主脚本一样的通用开关 python unlockmusic.py <二维码> --music 11414 --send --debug python unlockmusic.py <二维码> --music 11414 --send --cooldown 0 python unlockmusic.py <二维码> --music 11414 --send --no-verify | 参数 | 说明 | |---|---| | `--music ID[,ID...]` | 要解锁的 musicId,可重复、可逗号分隔(`--unlock-music` 也认) | | `--send` | 真正发包(不加就是预检,零副作用) | | `--no-unlock-master` | 不解禁 Master | | `--no-unlock-remaster` | 不解禁 Re:Master | | `--cooldown N` | 发包前冷却秒数,默认取 config 的 15 秒 | | `--no-verify` | 跳过上传后的落库回查 | | `--debug` | 本次运行打印完整收发报文 | 日志同样是 logs/latest.log(覆盖)+ logs/YYYY-MM-DD.log(追加)。 """ from __future__ import annotations import sys import upsert from api import log_error, setup_logging, teardown_logging def parse_args(argv): """极简命令行解析:第一个非 -- 开头的参数是二维码。""" opts = {"qr": "", "send": False, "debug": False, "no_verify": False, "cooldown": None, "extra_data": [], "unlock_music": [], "unlock_master": True, "unlock_remaster": True} i = 0 while i < len(argv): a = argv[i] if a == "--send": opts["send"] = True elif a == "--debug": opts["debug"] = True elif a == "--no-verify": opts["no_verify"] = True elif a == "--extra-data" and i + 1 < len(argv): opts["extra_data"].append(argv[i + 1]) i += 1 elif a == "--cooldown" and i + 1 < len(argv): opts["cooldown"] = float(argv[i + 1]) i += 1 elif a in ("--music", "--unlock-music") and i + 1 < len(argv): # 可重复;也支持逗号分隔(中英文逗号都认) for part in str(argv[i + 1]).replace(",", ",").split(","): part = part.strip() if not part: continue try: opts["unlock_music"].append(int(part)) except ValueError: opts["unlock_bad"] = part i += 1 elif a == "--no-unlock-master": opts["unlock_master"] = False elif a == "--no-unlock-remaster": opts["unlock_remaster"] = False elif not a.startswith("--") and not opts["qr"]: opts["qr"] = a i += 1 return opts def main() -> int: opts = parse_args(sys.argv[1:]) if not opts["qr"] or not opts["unlock_music"]: # 没有二维码、或者没有指定要解锁哪首 —— 直接把用法打出来 print(__doc__) return 1 if opts.get("unlock_bad"): # 参数本身就不对,连扫码都不用做 setup_logging("unlockmusic") log_error("--music 里有非法 musicId:" + str(opts["unlock_bad"])) teardown_logging() return 6 # ★ 整个流程复用 upsert.run(),这里只多传三个解锁参数 return upsert.run( opts, unlock_music=opts["unlock_music"], unlock_master=opts["unlock_master"], unlock_remaster=opts["unlock_remaster"], log_name="unlockmusic", ) if __name__ == "__main__": sys.exit(main())