网易云音乐歌单整理 Skill
概述
把用户的网易云歌单(典型场景:我喜欢的音乐,可能有上千首)按指定维度重新分类,并自动创建独立歌单。
两种分类维度(本 skill 均已跑通):
- 曲风:华语流行 / 粤语 / 摇滚 / 民谣 / R&B / 说唱 / 电子 / 国风古风 / 纯音乐 / 日韩 / 欧美。
- 情绪主旨:伤感失恋 / 甜蜜心动 / 治愈温暖 / 欢快律动 / 深情守候 / 释然洒脱 / 励志热血 / 怀旧思念 / 人生感悟 / 孤独落寞 / 江湖意境。
核心能力:
- 免扫码登录:直接从运行中的网易云桌面客户端(Electron)本地数据里解密出登录 Cookie。
- 匿名/登录态读歌单、歌曲详情、歌词、热评。
- weapi 加密直连官方接口,建歌单、加歌。
环境准备
- Python 3.13(venv 隔离),依赖:
pycryptodome、requests、qrcode、pillow。 - 安装(国内用清华镜像更快):
python -m venv <venv_path> <venv>/Scripts/python -m pip install pycryptodome requests qrcode pillow -i https://pypi.tuna.tsinghua.edu.cn/simple
工作流程
Phase 1:获取登录 Cookie(免扫码,推荐)
网易云桌面客户端是 Electron 应用,登录后的 Cookie 存在 Chromium 的 SQLite 里、用 Windows DPAPI 加密。
位置(Windows,用户 hyl 示例):
- Cookie 库:
C:\Users\<user>\AppData\Local\Netease\CloudMusic\webapp91x64\Cookies(SQLite,表cookies) - 解密密钥:同目录
LocalPrefs.json里的os_crypt.encrypted_key
解密三步:
- 读
LocalPrefs.json→os_crypt.encrypted_key(base64),去掉前 5 字节DPAPI前缀。 - 用
CryptUnprotectData(crypt32.dll,当前用户上下文)解出 32 字节 AES-256 密钥。 - 对
cookies表里每个encrypted_value(v10前缀 = 3 字节 + 12 字节 nonce + 密文 + 16 字节 GCM tag),用 AES-GCM 解密。
核心代码(extract_cookie.py):
import sqlite3, base64, json, ctypes
from ctypes import wintypes
from Crypto.Cipher import AES
class DATA_BLOB(ctypes.Structure):
_fields_ = [('cbData', wintypes.DWORD), ('pbData', ctypes.POINTER(ctypes.c_char))]
def dpapi_decrypt(data: bytes) -> bytes:
b_in = DATA_BLOB(len(data), ctypes.cast(ctypes.create_string_buffer(data), ctypes.POINTER(ctypes.c_char)))
b_out = DATA_BLOB()
ctypes.windll.crypt32.CryptUnprotectData(ctypes.byref(b_in), None, None, None, None, 0, ctypes.byref(b_out))
r = ctypes.string_at(b_out.pbData, b_out.cbData)
ctypes.windll.kernel32.LocalFree(b_out.pbData)
return r
def get_aes_key(prefs_path):
enc = base64.b64decode(json.load(open(prefs_path, encoding='utf-8'))['os_crypt']['encrypted_key'])
return dpapi_decrypt(enc[5:]) # 去 'DPAPI' 前缀
def decrypt_cookie(enc_value: bytes, key: bytes) -> str:
d = enc_value[3:] # 去 'v10'
nonce, ct = d[:12], d[12:]
return AES.new(key, AES.MODE_GCM, nonce=nonce).decrypt_and_verify(ct[:-16], ct[-16:]).decode('utf-8', 'replace')
需要的 Cookie 字段:MUSIC_U(主凭证)、__csrf,可附带 MUSIC_A、os=pc 等。
若不想/无法读本地文件,可退回二维码登录,但注意接口已变更(见「踩坑」)。
Phase 2:读取歌单 + 歌曲详情(可匿名)
- 用户歌单列表:
GET https://music.163.com/api/user/playlist?uid=<uid>&limit=1000&offset=0- 「我喜欢的音乐」通常是第一个 trackCount 最大的歌单;用户 UID 可从客户端
info文件或登录态/weapi/w/nuser/account/get拿到。
- 「我喜欢的音乐」通常是第一个 trackCount 最大的歌单;用户 UID 可从客户端
- 歌单歌曲 ID:
GET https://music.163.com/api/v6/playlist/detail?id=<playlistId>&n=2000&s=0→playlist.trackIds - 歌曲详情(批量 ≤200/次):
POST https://music.163.com/api/song/detail,bodyids=[1,2,3](form-urlencoded)
这些「旧版」公开接口目前仍可用、且无需加密。
Phase 3:抓歌词 + 热评(登录态 weapi)
情绪分类必须拿到歌词和热评:
- 歌词:
POST /weapi/song/lyric,body{id, lv:1, kv:1, tv:-1}→lrc.lyric(需清洗[mm:ss.xx]时间戳与「作词/作曲」元信息行) - 热评:
POST /weapi/v1/resource/comments/R_SO_4_{id},body{rid, limit:12, offset:0, orderType:1}→hotComments+comments
Phase 4:分类
曲风维度(无歌词也可做):语言检测 + 歌手→流派映射 + 标题关键词 + 纯音乐兜底。
- 语言:日文假名/韩文谚文/纯拉丁/汉字分别判为日/韩/欧美/华语。
- 流派:对 Top 歌手建映射表(Beyond→摇滚、陶喆→R&B、赵雷→民谣…),覆盖绝大多数歌曲;长尾歌手回落到「流行」。
情绪维度(更准,但更费劲):强烈建议逐首人工通读判定。
- 关键词打分对「爱情类」歌(甜蜜 vs 伤感 vs 深情)误判率极高——「我爱你」既出现在甜歌也出现在苦情歌里;「快乐」会被「你不是真正的快乐」这种否定句骗到。
- 可靠做法:批量抓歌词+热评 → 生成复核清单(每首一行:歌名/歌手/歌词摘要/热评摘要)→ 逐首人工判定(热评是极好的情绪佐证,如"意难平/听哭了"→伤感,"太甜了/恋爱了"→甜蜜)。
- 纯音乐/影视配乐统一归「治愈温暖」。
Phase 5:建歌单 + 批量加歌(weapi,登录态)
- 建歌单:
POST /weapi/playlist/create,body{name, privacy:0, type:'NORMAL'}→id- 注意是
/weapi/...,不是/api/...(/api/playlist/create会返回 400「请求参数错误」)。
- 注意是
- 加歌:
POST /weapi/playlist/manipulate/tracks,body{op:'add', pid, trackIds:'["id1","id2"]', imme:'true'}- 若返回
code 512,降级到/api/playlist/manipulate/tracks且trackIds每个 id 重复一次。
- 若返回
- 删歌单:
POST /weapi/playlist/remove,body{ids:'[pid]', pid}
关键技术:weapi 加密(Python 实现)
网易云 weapi 是「AES-128-CBC 双层加密 + RSA 封装密钥」,用 pycryptodome 实现:
import json, os, base64
from Crypto.Cipher import AES
from Crypto.PublicKey import RSA
from Crypto.Util.Padding import pad
PRESET_KEY = '0CoJUm6Qyw8W8jud'
IV = '0102030405060708'
BASE62 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
PUBLIC_KEY = ('-----BEGIN PUBLIC KEY-----\n'
'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37'
'BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV'
'8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44on'
'caTWz7OBGLbCiK45wIDAQAB\n-----END PUBLIC KEY-----')
def _aes(text: str, key: str, iv: str) -> bytes:
return AES.new(key.encode(), AES.MODE_CBC, iv.encode()).encrypt(pad(text.encode(), 16))
def weapi_encrypt(obj: dict) -> dict:
text = json.dumps(obj, ensure_ascii=False)
secret = ''.join(BASE62[os.urandom(1)[0] % 62] for _ in range(16))
inner = base64.b64encode(_aes(text, PRESET_KEY, IV)).decode()
params = base64.b64encode(_aes(inner, secret, IV)).decode()
k = RSA.import_key(PUBLIC_KEY)
m = int.from_bytes(b'\x00' * (128 - 16) + secret[::-1].encode(), 'big') # RSA_NO_PADDING,左补零到 128 字节
enc_sec_key = format(pow(m, k.e, k.n), 'x')
return {'params': params, 'encSecKey': enc_sec_key}
请求方式:POST https://music.163.com<path>,body 为上述两个字段(form-urlencoded),Header 带 Cookie + Referer: https://music.163.com/。
踩坑记录(重点!)
- 二维码登录接口已变更:轮询端点从
/weapi/login/qrcode/clientlogin改为/weapi/login/qrcode/client/login(多一个/);且type=1会返回8821「请切换其他登录方式或升级新版本再试」(旧客户端被弃用)。首选直接读桌面客户端 Cookie,别走扫码。 - 建歌单接口是
/weapi/playlist/create(不是/api/...)。 - 频率限制:批量建歌单/加歌会触发
code 405「操作过于频繁」,需加 6–8 秒间隔 + 重试;偶发空响应(JSONDecodeError)也要try/except重试,不能只判断 code。 - 关键词分类偷懒会导致大量误判:情绪分类务必逐首通读歌词+热评;热评是情绪佐证金矿。
- 下架歌曲:个别歌返回
401「下架歌曲无法收藏」,属正常,跳过即可。
脚本清单
完整可复用脚本见本 skill 的 scripts/ 目录:
| 文件 | 作用 |
|---|---|
| netease_client.py | weapi 加密 + NeteaseClient(_weapi / qr_unikey / qr_check) |
| extract_cookie.py | DPAPI 解密桌面客户端 Cookie → cookie.txt |
| fetch_songs.py | 拉歌单 + 歌曲详情 → songs.json |
| fetch_lyrics.py | 批量拉歌词 → lyrics.json |
| fetch_comments.py | 批量拉热评 → comments.json |
| make_review.py | 生成人工复核清单 → review.txt |
| classify.py | 曲风分类(歌手映射 + 语言检测) |
| classify_emotion.py | 情绪分类(关键词基线,供参考;最终以人工复核为准) |
| create_playlists.py | 建歌单 + 加歌(含频率限制重试) |
脚本内路径为硬编码,复用时替换为实际工作目录即可。
交付与收尾
- 分类结果用
present_files或内联图表展示分布。 - 敏感提示:
cookie.txt是登录凭证,用完提醒用户删除。 - 若用户对某分类不满意,直接「删旧歌单 → 重建」是最干净的做法(
/weapi/playlist/remove逐条删除,再重建)。
微信扫一扫