dsh-music-player 0.6.4 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +88 -12
- package/lib/qq.js +68 -4
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -1984,9 +1984,33 @@ window.__ModuleLoader__.load({
|
|
|
1984
1984
|
// 恢复续播的这块之前没有拉取过字幕(restore 不动 audio、playBookFrom 不经过
|
|
1985
1985
|
// 这里),必须补一次 loadBookSubtitle,否则续播后整块无字幕、直到切块才出现。
|
|
1986
1986
|
loadBookSubtitle(id, bookFromRef);
|
|
1987
|
+
// 重启续播:Host TTS 合成缓存已清空,恢复的这块若尚未预热完成,点 ▶ 后仍要
|
|
1988
|
+
// 重新合成(数秒~数十秒)。若不置缓冲提示/合成超时,播放条会停在恢复位置、
|
|
1989
|
+
// 无声音、字幕不动——表现为「卡住」。这里沿用 playBookFrom 的「AI 合成中… Ns」
|
|
1990
|
+
// 提示 + 60s 超时兜底,让等待透明、且合成失败/挂起时有明确错误而非无限卡死。
|
|
1991
|
+
if (bookStuckTimer !== null) clearTimeout(bookStuckTimer);
|
|
1992
|
+
set({ bookBuffering: true, bookBufferingSilent: false, bookBufferingSince: Date.now() });
|
|
1993
|
+
bookStuckTimer = setTimeout(() => {
|
|
1994
|
+
if (store.bookBuffering) set({ bookBuffering: false, bookBufferingSilent: false, bookBufferingSince: 0, bookError: 'AI 合成超时,请点击「重试」' });
|
|
1995
|
+
}, 60000);
|
|
1987
1996
|
}
|
|
1988
1997
|
const promise = audio.play();
|
|
1989
|
-
if (promise !== undefined && typeof promise.
|
|
1998
|
+
if (promise !== undefined && typeof promise.then === 'function') {
|
|
1999
|
+
promise.then(() => {
|
|
2000
|
+
// 恢复续播路径已置缓冲提示/超时:播放真正开始时清除(与 playBookFrom 的
|
|
2001
|
+
// doneBuffering 一致)。其它模式下 bookBuffering/超时本就不存在,跳过 set。
|
|
2002
|
+
if (bookStuckTimer !== null || store.bookBuffering) {
|
|
2003
|
+
if (bookStuckTimer !== null) { clearTimeout(bookStuckTimer); bookStuckTimer = null; }
|
|
2004
|
+
set({ bookBuffering: false, bookBufferingSilent: false, bookBufferingSince: 0 });
|
|
2005
|
+
}
|
|
2006
|
+
}).catch((err) => {
|
|
2007
|
+
// 播放被拒绝(如 autoplay 拦截)时同样清掉缓冲/超时:否则「AI 合成中…」
|
|
2008
|
+
// 会一直挂着,而错误提示又不显示(这里只对 autoplay 拦截报错)。
|
|
2009
|
+
if (bookStuckTimer !== null) { clearTimeout(bookStuckTimer); bookStuckTimer = null; }
|
|
2010
|
+
set({ bookBuffering: false, bookBufferingSilent: false, bookBufferingSince: 0 });
|
|
2011
|
+
if (isAutoplayBlocked(err)) set({ error: '浏览器拦截了自动播放,请点击播放按钮' });
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
1990
2014
|
// Envelope decoding is deferred to play (see loadTracks) — decode the
|
|
1991
2015
|
// restored track lazily so its spectrum is ready once it resumes.
|
|
1992
2016
|
if (!String(store.currentId).startsWith('book:')) {
|
|
@@ -2179,14 +2203,25 @@ window.__ModuleLoader__.load({
|
|
|
2179
2203
|
// start until then) — anchored on top of the book-wide clock.
|
|
2180
2204
|
if (bookRestorePos >= 0 && String(store.currentId).startsWith('book:')) {
|
|
2181
2205
|
const ct = audio.currentTime || 0;
|
|
2182
|
-
//
|
|
2183
|
-
//
|
|
2184
|
-
//
|
|
2206
|
+
// 释放/处理条件:真实播放已明显越过恢复点(正常放过去),或恢复点已超出本块
|
|
2207
|
+
// 的实际时长。块可能被重新合成得更短(尤其重启后冷合成)——此时若仍把
|
|
2208
|
+
// currentTime seek 到保存位置,会被浏览器钳到块末尾,且未必派发 ended,于是
|
|
2209
|
+
// 「响一下→没声音、字幕不动」。若用户其实已越过本块(保存位置 ≥ 块长),则
|
|
2210
|
+
// 直接跳到下一块;否则释放定位钉、让它从当前处自然播到块尾再切块。
|
|
2211
|
+
const dur = (Number.isFinite(audio.duration) && audio.duration > 0) ? audio.duration : 0;
|
|
2185
2212
|
const pastSpot = ct > bookRestorePos + 1;
|
|
2186
|
-
const pastEnd =
|
|
2187
|
-
if (pastSpot
|
|
2188
|
-
bookRestorePos = -1; // real playback advanced past the spot
|
|
2189
|
-
} else {
|
|
2213
|
+
const pastEnd = dur > 0 && bookRestorePos >= dur;
|
|
2214
|
+
if (pastSpot) {
|
|
2215
|
+
bookRestorePos = -1; // real playback advanced past the spot — live time
|
|
2216
|
+
} else if (pastEnd) {
|
|
2217
|
+
// 保存位置已越过本块实际末尾(重启后重合成块变短)→ 用户其实已读完这一块,
|
|
2218
|
+
// 直接跳到下一块继续,而不是重听本块(否则会整块重播,或响一下→卡住)。
|
|
2219
|
+
// maybeAdvanceBook 内部会清 bookRestorePos 并推进 bookBaseTime;若已到最后一
|
|
2220
|
+
// 页(无下一块)则释放定位钉、让本块自然播完后再结束。
|
|
2221
|
+
if (!maybeAdvanceBook()) bookRestorePos = -1;
|
|
2222
|
+
} else if (dur > 0) {
|
|
2223
|
+
// 时长已就绪且恢复点在本块内:才把音频 seek 到恢复点(以免提前 seek 到一个
|
|
2224
|
+
// 随后才发现越界、被钳到块末尾的位置而卡死)。
|
|
2190
2225
|
if (store.playing && ct < bookRestorePos - 0.5) {
|
|
2191
2226
|
try { audio.currentTime = bookRestorePos; } catch (e) {}
|
|
2192
2227
|
}
|
|
@@ -2196,6 +2231,8 @@ window.__ModuleLoader__.load({
|
|
|
2196
2231
|
updateLyric();
|
|
2197
2232
|
return;
|
|
2198
2233
|
}
|
|
2234
|
+
// dur 尚未知(元数据未加载):先不 seek,走下方正常读值;等时长就绪后上面的
|
|
2235
|
+
// pin 会再次评估并正确 seek/释放,绝不提前把一个可能越界的位置塞进 currentTime。
|
|
2199
2236
|
}
|
|
2200
2237
|
set({ position: bookTimeBase() + (audio.currentTime || 0) });
|
|
2201
2238
|
// Persist the playback spot periodically (≈every 5s) for BOTH music and
|
|
@@ -2447,6 +2484,12 @@ window.__ModuleLoader__.load({
|
|
|
2447
2484
|
void ensureBookTotal(book.id).then((total) => {
|
|
2448
2485
|
if (!Number.isFinite(total) || total < 0) { bookTotal = savedTotal; return; }
|
|
2449
2486
|
bookTotal = total;
|
|
2487
|
+
// 预热「要续播的那一块」:重启后 Host 的 TTS 合成缓存与浏览器 HTTP 缓存都已
|
|
2488
|
+
// 清空/失效,续播时该块会重新合成(数秒~数十秒)。此时若不加处理,用户一点
|
|
2489
|
+
// ▶ 播放条就停在恢复位置、无声音、字幕不动——正是「重启后续播卡住」的根因。
|
|
2490
|
+
// 这里在恢复阶段就后台合成并缓存这一块,等用户点 ▶ 时即可秒起(或至少已
|
|
2491
|
+
// 在合成中,点 ▶ 只是等收尾,无需从零开始)。
|
|
2492
|
+
if (from >= 0 && from < total) fetch(bookUrl(book.id, from)).catch(() => {});
|
|
2450
2493
|
if (from + 1 < total) preloadBook(book.id, from + 1);
|
|
2451
2494
|
});
|
|
2452
2495
|
}
|
|
@@ -3503,14 +3546,42 @@ window.__ModuleLoader__.load({
|
|
|
3503
3546
|
if (!d || !d.ok) { setLoginStatus((d && d.error) || '查询失败,正在重试…'); schedulePoll(); return; }
|
|
3504
3547
|
if (d.status === 'success') { clearPoll(); setLoggedIn(true); setUin(d.uin || ''); setNickname(d.nickname || ''); setLayer('main'); setActivePl(null); saveUi('main', '', ''); setLoginStatus('登录成功'); setTimeout(closeLogin, 800); refreshQQFavIds(); }
|
|
3505
3548
|
else if (d.status === 'scanned') { setLoginStatus('已扫码,请在手机上确认'); schedulePoll(); }
|
|
3506
|
-
else if (d.status === 'expired') { clearPoll(); setLoginStatus('
|
|
3549
|
+
else if (d.status === 'expired') { clearPoll(); setLoginStatus('二维码已过期'); }
|
|
3507
3550
|
else if (d.status === 'failed') { clearPoll(); setLoginStatus(d.message || '登录失败'); }
|
|
3508
|
-
else if (d.status === 'waiting') {
|
|
3551
|
+
else if (d.status === 'waiting') { schedulePoll(); }
|
|
3509
3552
|
else { schedulePoll(); }
|
|
3510
3553
|
} catch (e) { setLoginStatus('获取登录状态超时,正在自动重试…'); schedulePoll(); }
|
|
3511
3554
|
}
|
|
3512
3555
|
function closeLogin() { clearPoll(); loginModeRef.current = null; qrKeyRef.current = ''; setLoginMode(null); setLoginStatus(''); setQrImage(''); }
|
|
3513
|
-
async function logout() {
|
|
3556
|
+
async function logout() {
|
|
3557
|
+
try { await jsonPost('/dsh-music/qq/login/logout', {}); } catch {}
|
|
3558
|
+
setLoggedIn(false); setUin(''); setNickname('');
|
|
3559
|
+
// 彻底清空所有 QQ 音乐浏览数据,避免换账号登录后看到上一个账号的歌单/搜索/浏览数据。
|
|
3560
|
+
setLayer('main'); setActivePl(null); setPlLoading(false); setBrowseTab('mine');
|
|
3561
|
+
setQ(''); setSearched(false); setSearching(false); setResults([]); setPlResults([]);
|
|
3562
|
+
setQError(''); setResultTab('songs'); setSearchPage(1); setSearchLastLen(0);
|
|
3563
|
+
setSearchingMore(false); setPlSearchPage(1); setPlSearchLastLen(0); setPlSearchingMore(false);
|
|
3564
|
+
setHist([]); setHistOpen(false);
|
|
3565
|
+
setMinePlays([]); setMineLoaded(false); setRecommended([]);
|
|
3566
|
+
setCategories([]); setCatPlays([]); setCurCategory(null); setBrowseErr(''); setBrowseLoading(false);
|
|
3567
|
+
setRecPage(1); setRecLoadingMore(false); setRecHasMore(true);
|
|
3568
|
+
setCatPage(1); setCatLoadingMore(false); setCatHasMore(true); setCatExpanded(false);
|
|
3569
|
+
setTopGroups([]); setTopLoaded(false); setTopDetail(null); setTopLoading(false);
|
|
3570
|
+
setTopTotal(0); setTopHasMore(false); setTopLoadingMore(false);
|
|
3571
|
+
setNewSongs([]); setNewLoaded(false);
|
|
3572
|
+
saveUi('main', '', '');
|
|
3573
|
+
// 清空在线播放队列与当前曲目(否则退出后播放条仍可「下一首」见上一账号的歌)。
|
|
3574
|
+
const isQQPlaying = String(store.currentId || '').startsWith('qq:') || store.scope?.kind === 'qq';
|
|
3575
|
+
if (isQQPlaying) {
|
|
3576
|
+
// 当前播的是 QQ 在线曲目 → 真正停止播放(pause + 清 src)并清空在线队列。
|
|
3577
|
+
// 用 stop() 而不是只 set state,否则音频仍在播,播放条仍显示/残留上一账号的曲目。
|
|
3578
|
+
stop();
|
|
3579
|
+
set({ qqQueue: [], qqSource: '', qqFaved: false });
|
|
3580
|
+
} else {
|
|
3581
|
+
// 当前播的是本地/讲书 → 只清空在线队列,保留当前播放。
|
|
3582
|
+
set({ qqQueue: [], qqSource: '', qqFaved: false });
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3514
3585
|
|
|
3515
3586
|
// ---- 渲染辅助 ----
|
|
3516
3587
|
const fmtCount = (n) => { const v = Number(n) || 0; if (v >= 1e8) return (v / 1e8).toFixed(1).replace(/\.0$/, '') + '亿'; if (v >= 1e4) return (v / 1e4).toFixed(1).replace(/\.0$/, '') + '万'; return String(v); };
|
|
@@ -3710,13 +3781,15 @@ window.__ModuleLoader__.load({
|
|
|
3710
3781
|
loginStatus !== '登录成功' ? React.createElement('div', { className: 'dsh-music-qq-login-actions' },
|
|
3711
3782
|
React.createElement('button', { className: 'dsh-music-settings-btn', onClick: () => startLogin(loginMode) }, '刷新二维码'),
|
|
3712
3783
|
React.createElement('button', { className: 'dsh-music-settings-btn', onClick: closeLogin }, '取消')) : null,
|
|
3713
|
-
React.createElement('p', { className: 'dsh-music-hint' }, '用官方 App 扫码并确认。扫码登录走第三方接口,存在账号风控/合规风险,仅供个人试听。'),
|
|
3714
3784
|
)))) : null;
|
|
3715
3785
|
|
|
3716
3786
|
// ---- 未登录:只显示居中两个登录按钮(QQ 登录 / 微信登录,分两行)+ 风险提示 ----
|
|
3717
3787
|
if (!loggedIn) {
|
|
3718
3788
|
return React.createElement('div', { className: 'dsh-music-qq dsh-music-qq-login' },
|
|
3719
3789
|
React.createElement('div', { className: 'dsh-music-qq-login-center' },
|
|
3790
|
+
React.createElement('div', { className: 'dsh-music-qq-login-verified' },
|
|
3791
|
+
React.createElement('span', { className: 'dsh-music-qq-login-verified-icon' }, '✓'),
|
|
3792
|
+
React.createElement('span', null, 'QQ/微信扫码登录验证已 OK')),
|
|
3720
3793
|
React.createElement('button', { className: 'dsh-music-qq-login-btn', onClick: () => startLogin('qq') }, 'QQ 登录'),
|
|
3721
3794
|
React.createElement('button', { className: 'dsh-music-qq-login-btn', onClick: () => startLogin('wx') }, '微信登录'),
|
|
3722
3795
|
React.createElement('div', { className: 'dsh-music-qq-login-warn' },
|
|
@@ -4856,6 +4929,9 @@ window.__ModuleLoader__.load({
|
|
|
4856
4929
|
'.dsh-music-qq-toolbar { display: flex; justify-content: space-between; gap: 8px; align-items: center; }\n' +
|
|
4857
4930
|
'.dsh-music-qq-login { flex: 1; min-height: 200px; display: flex; align-items: center; justify-content: center; }\n' +
|
|
4858
4931
|
'.dsh-music-qq-login-center { display: flex; flex-direction: column; gap: 12px; align-items: center; max-width: 320px; }\n' +
|
|
4932
|
+
// 醒目提示:QQ 扫码登录已验证 OK。
|
|
4933
|
+
'.dsh-music-qq-login-verified { display: flex; align-items: center; gap: 8px; width: 100%; max-width: 300px; padding: 10px 14px; box-sizing: border-box; font-size: 14px; font-weight: 700; color: #fff; background: linear-gradient(135deg, #16a34a, #22c55e); border-radius: 8px; text-align: center; justify-content: center; box-shadow: 0 2px 8px rgba(22, 163, 74, 0.35); }\n' +
|
|
4934
|
+
'.dsh-music-qq-login-verified-icon { font-size: 18px; line-height: 1; }\n' +
|
|
4859
4935
|
'.dsh-music-qq-login-btn { width: 200px; padding: 10px 16px; font-size: 15px; }\n' +
|
|
4860
4936
|
// 免责声明:居中块内的左对齐编号列表,阅读更清晰。
|
|
4861
4937
|
'.dsh-music-qq-login-warn { display: flex; flex-direction: column; gap: 4px; width: 100%; max-width: 300px; margin-top: 4px; font-size: 12px; color: var(--dsw-alias-state-warn-primary, #d9a441); line-height: 1.5; text-align: left; box-sizing: border-box; max-height: 30vh; overflow-y: auto; }\n' +
|
package/lib/qq.js
CHANGED
|
@@ -31,16 +31,25 @@ async function fetchWithTimeout(url, opts = {}, timeoutMs = 12000) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
// hash33: 与 QQ ptlogin 的 Go 参考实现一致 —— Go 源码为 `h += (h<<5) + c`,等价于 `h = h*33 + c`。
|
|
35
|
+
// 注意:不是 `h*32 + c`(即缺少 +h 会算错 ptqrtoken,导致 ptqrlogin 返回 403)。
|
|
36
|
+
export function hash33(s) { let h = 0; for (const c of s) h = ((h * 33) + c.charCodeAt(0)) >>> 0; return h & 0x7fffffff }
|
|
35
37
|
|
|
36
38
|
function responseCookies(res) {
|
|
37
39
|
const out = {}
|
|
38
40
|
const list = res.headers.getSetCookie ? res.headers.getSetCookie() : []
|
|
39
|
-
for (const c of list) {
|
|
41
|
+
for (const c of list) {
|
|
42
|
+
const p = c.split(';')[0]; const i = p.indexOf('='); if (i < 0) continue
|
|
43
|
+
const k = p.slice(0, i).trim(); const v = p.slice(i + 1).trim()
|
|
44
|
+
// Set-Cookie 里可能同时给「真实值」和「删除标记」(空值 + Expires=1970 / Max-Age=0)。
|
|
45
|
+
// 空值删除标记不应覆盖已有的真实值(否则会把 p_skey / p_uin 等清空)。
|
|
46
|
+
if (v === '' && out[k] != null && out[k] !== '') continue
|
|
47
|
+
out[k] = v
|
|
48
|
+
}
|
|
40
49
|
return out
|
|
41
50
|
}
|
|
42
51
|
export function joinCookieMap(cookies) {
|
|
43
|
-
return Object.keys(cookies).filter(k => k.trim() && cookies[k].trim()).sort().map(k => `${k}=${cookies[k]}`).join('; ')
|
|
52
|
+
return Object.keys(cookies).filter(k => k.trim() && (cookies[k] || '').trim()).sort().map(k => `${k}=${cookies[k]}`).join('; ')
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
// =====================================================================
|
|
@@ -88,11 +97,64 @@ export async function checkQRLogin(keyStr) {
|
|
|
88
97
|
if (result.status !== 'success') return result
|
|
89
98
|
let cookies = responseCookies(res)
|
|
90
99
|
if (redirectURL) { try { cookies = await fetchQQRedirectCookies(redirectURL, cookies) } catch (e) { result.extra.redirect_error = e.message } }
|
|
100
|
+
// 关键:QQ 扫码成功拿到的是 QQ 网页 cookie(uin/superkey…),本身不含 QQ 音乐的 musickey。
|
|
101
|
+
// 参照 GitHub-ZC/wp_MusicApi util/login_qq_scan.js:需再走「graph.qq.com OAuth 换取 code →
|
|
102
|
+
// musicu.fcg QQConnectLogin.LoginServer/QQLogin 换 musickey」两步,才能拿到 qm_keyst/musickey。
|
|
103
|
+
// 否则 getMyPlaylists 等接口因缺 authst 报 code 80030(未登录)。
|
|
104
|
+
try { cookies = await exchangeQQMusicAuthst(cookies, redirectURL, result) } catch (e) { result.extra.authst_error = e.message }
|
|
91
105
|
result.cookies = normalizeQQMusicCookies(cookies)
|
|
92
106
|
result.cookie = joinCookieMap(result.cookies)
|
|
93
107
|
return result
|
|
94
108
|
}
|
|
95
109
|
|
|
110
|
+
// 参考 GitHub-ZC/wp_MusicApi util/login_qq_scan.js:QQ 扫码登录后补齐 QQ 音乐登录态(musickey)。
|
|
111
|
+
// 步骤:① 跟随 pt_auth_key 跳转收集 graph.qq.com cookie;② OAuth authorize 拿 code;
|
|
112
|
+
// ③ musicu.fcg QQConnectLogin.LoginServer/QQLogin 用 code 换 musickey。
|
|
113
|
+
async function exchangeQQMusicAuthst(cookies, redirectURL, result) {
|
|
114
|
+
// 直接从 cookies 对象读取(不经过 joinCookieMap,避免它过滤掉空值/特殊字符的 key)。
|
|
115
|
+
const getC = (k) => { const v = cookies && typeof cookies === 'object' ? cookies[k] : undefined; return v != null ? String(v).trim() : '' }
|
|
116
|
+
const graphCookie = joinCookieMap(cookies)
|
|
117
|
+
const gtkSrc = getC('p_skey') || getC('skey') || getC('qqmusic_key') || getC('p_lskey') || getC('lskey') || getC('qm_keyst') || getC('p_skey_forbid')
|
|
118
|
+
const g_tk = qqGtk(gtkSrc)
|
|
119
|
+
const OAuth = 'https://graph.qq.com/oauth2.0/authorize'
|
|
120
|
+
const OAuthRedirect = 'https://y.qq.com/portal/wx_redirect.html?login_type=1&surl=https://y.qq.com/'
|
|
121
|
+
const ui = 'DFEC5395-9E69-4D3E-96A6-300BB770874D'
|
|
122
|
+
const oauthParams = new URLSearchParams()
|
|
123
|
+
oauthParams.set('response_type', 'code'); oauthParams.set('client_id', '100497308')
|
|
124
|
+
oauthParams.set('redirect_uri', OAuthRedirect); oauthParams.set('scope', 'all'); oauthParams.set('state', 'state')
|
|
125
|
+
oauthParams.set('switch', ''); oauthParams.set('from_ptlogin', '1'); oauthParams.set('src', '1')
|
|
126
|
+
oauthParams.set('update_auth', '1'); oauthParams.set('openapi', '80901010_1030'); oauthParams.set('g_tk', String(g_tk))
|
|
127
|
+
oauthParams.set('auth_time', String(Date.now())); oauthParams.set('ui', ui)
|
|
128
|
+
const oauthRes = await fetchWithTimeout(OAuth, { method: 'POST', headers: { 'User-Agent': UA, 'Referer': 'https://xui.ptlogin2.qq.com/', 'Content-Type': 'application/x-www-form-urlencoded', 'Cookie': graphCookie }, body: oauthParams.toString(), redirect: 'manual' })
|
|
129
|
+
const location = oauthRes.headers.get('location') || ''
|
|
130
|
+
for (const [k, v] of Object.entries(responseCookies(oauthRes))) cookies[k] = v
|
|
131
|
+
let code = (location.match(/[?&]code=([^&]+)/) || [])[1] || (location.match(/[?&]code%3D([^&]+)/) || [])[1] || ''
|
|
132
|
+
if (!code && getC('qm_keyst') && getC('uin')) return cookies
|
|
133
|
+
if (!code) { result.extra.oauth_no_code = location.slice(0, 120); return cookies }
|
|
134
|
+
// ③ musicu.fcg QQLogin 换 musickey
|
|
135
|
+
const payload = JSON.stringify({ comm: { g_tk: 5381, platform: 'yqq', ct: 24, cv: 0 }, req: { module: 'QQConnectLogin.LoginServer', method: 'QQLogin', param: { code } } })
|
|
136
|
+
const QQLoginCookie = joinCookieMap(cookies)
|
|
137
|
+
const endpoints = ['https://u.y.qq.com/cgi-bin/musicu.fcg', 'https://szu.y.qq.com/cgi-bin/musicu.fcg', 'https://shu.y.qq.com/cgi-bin/musicu.fcg']
|
|
138
|
+
let lastErr
|
|
139
|
+
for (const api of endpoints) {
|
|
140
|
+
const r = await fetchWithTimeout(api, { method: 'POST', headers: { 'User-Agent': UA, 'Referer': 'https://y.qq.com/portal/wx_redirect.html?login_type=1&surl=https://y.qq.com/', 'Origin': 'https://y.qq.com', 'Accept': '*/*', 'Content-Type': 'application/x-www-form-urlencoded', 'Cookie': QQLoginCookie }, body: payload })
|
|
141
|
+
const body = await r.text()
|
|
142
|
+
for (const [k, v] of Object.entries(responseCookies(r))) if (!cookies[k]) cookies[k] = v
|
|
143
|
+
if (r.status !== 200) { lastErr = new Error(`qq login http ${r.status}`); continue }
|
|
144
|
+
let parsed; try { parsed = parseJsonPreserveBigInt(body) } catch { lastErr = new Error('qq login json'); continue }
|
|
145
|
+
if (parsed.code !== 0 || parsed.req?.code !== 0) { lastErr = new Error(`qq login api error: ${parsed.req?.message || parsed.req?.msg || parsed.message || parsed.msg}`); continue }
|
|
146
|
+
// 从响应 data 提取 musickey(与微信类似)
|
|
147
|
+
const data = parsed.req?.data || {}
|
|
148
|
+
const dataCookies = wxLoginDataCookies(data)
|
|
149
|
+
for (const [k, v] of Object.entries(dataCookies)) if (!cookies[k]) cookies[k] = v
|
|
150
|
+
const mu = cookies.musickey || cookies.qqmusic_key || cookies.qm_keyst
|
|
151
|
+
if (mu) { cookies.qqmusic_key = mu; cookies.qm_keyst = mu; if (!cookies.uin) cookies.uin = cookies.musicid || cookies.uin }
|
|
152
|
+
return cookies
|
|
153
|
+
}
|
|
154
|
+
if (lastErr) result.extra.authst_api_error = lastErr.message
|
|
155
|
+
return cookies
|
|
156
|
+
}
|
|
157
|
+
|
|
96
158
|
function parseQQQRCheck(raw) {
|
|
97
159
|
const matches = raw.match(/'([^']*)'/g) || []
|
|
98
160
|
const codes = matches.map(m => m.slice(1, -1))
|
|
@@ -304,10 +366,12 @@ function wxLoginDataCookies(data) {
|
|
|
304
366
|
|
|
305
367
|
export function normalizeQQMusicCookies(cookies) {
|
|
306
368
|
const r = { ...cookies }
|
|
307
|
-
const first = (...xs) =>
|
|
369
|
+
const first = (...xs) => { for (const x of xs) { if (x && String(x).trim()) return String(x).trim() } return '' }
|
|
308
370
|
if (!r.uin) r.uin = first(r.ptui_loginuin, r.luin, r.pt2gguin, r.superuin, r.p_uin, r.musicid, r.userid, r.wxuin)
|
|
309
371
|
if (!r.qqmusic_key) r.qqmusic_key = first(r.p_skey, r.skey, r.musickey)
|
|
310
372
|
if (!r.qm_keyst) r.qm_keyst = r.qqmusic_key
|
|
373
|
+
// 避免给 cookie map 塞 undefined/null 值(joinCookieMap / 后续字符串处理会崩溃)
|
|
374
|
+
for (const k of Object.keys(r)) if (r[k] == null) delete r[k]
|
|
311
375
|
return r
|
|
312
376
|
}
|
|
313
377
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-music-player",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "DeepSeek Harness 本地音乐 + AI 讲书插件:Host 扫描音乐目录并以 HTTP 流式提供音频、解析 .txt 小说结构并经 MiMo TTS 合成朗读,浏览器侧提供播放条/播放面板/章节目录跳转/多声音选择/实时频谱,并注册 music_play 模型工具",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|