dsh-music-player 0.5.3 → 0.5.4
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/README.md +3 -3
- package/lib/client.js +162 -33
- package/lib/index.js +28 -1
- package/lib/qq.js +52 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -112,13 +112,13 @@ dsh plugin --profile <profile> add ./dsh-music-player-0.1.0.tgz
|
|
|
112
112
|
|
|
113
113
|
- **登录**:两种扫码方式——**QQ 登录**或**微信登录**(推荐)。登录态保存在 Host 端(`~/.dsh/music-player-qq-cookie.json`),刷新/重启不丢;面板右上角可退出登录。
|
|
114
114
|
- **浏览**:6 个子页签——**我的歌单 / 推荐歌单 / 分类歌单 / 排行榜 / 新歌 / 搜索**。
|
|
115
|
-
-
|
|
115
|
+
- 我的歌单:登录后展示当前账号的歌单(卡片式),本人创建的歌单卡片右上角可一键删除(二次确认;「我喜欢」不可删除)。
|
|
116
116
|
- 推荐歌单:热门推荐 12 条,底部「加载更多」可续载。
|
|
117
117
|
- 分类歌单:60+ 分类(默认折叠显示 8 个,可展开),每个分类的歌单支持「加载更多」。
|
|
118
|
-
-
|
|
118
|
+
- 排行榜:巅峰榜/地区榜/特色榜等分组,点榜单看歌曲(带榜单封面卡片),榜单详情底部「加载更多」可分页续载全部歌曲。
|
|
119
119
|
- 新歌:新歌速递(最新/内地/港台/欧美/韩国等)。
|
|
120
120
|
- 搜索:搜歌曲与歌单,带搜索历史(localStorage,最近 10 条)。
|
|
121
|
-
- **播放**:点击任意歌曲即可播放(同一播放条 + 频谱);VIP
|
|
121
|
+
- **播放**:点击任意歌曲即可播放(同一播放条 + 频谱);VIP 标识显示在歌名后,行尾显示歌手名。进入歌单/播放列表时自动定位到正在播放的那一首,且正在播放的条目以高亮选中态显示。
|
|
122
122
|
- **收藏**:播放条爱心按钮把当前在线曲目收藏到 QQ 音乐「我喜欢」,已收藏歌曲爱心实时点亮。
|
|
123
123
|
- **续播**:在线播放进度(当前曲目 + 队列)刷新后自动恢复,点 ▶ 续播。
|
|
124
124
|
- 在线曲目不占本地曲库的 500 首上限,与本地/讲书完全隔离。
|
package/lib/client.js
CHANGED
|
@@ -1696,6 +1696,16 @@ window.__ModuleLoader__.load({
|
|
|
1696
1696
|
return React.createElement('svg', { className: cls, width: 12, height: 12, viewBox: '0 0 24 24', fill: 'currentColor', 'aria-hidden': true },
|
|
1697
1697
|
React.createElement('path', { d: 'M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z' }));
|
|
1698
1698
|
}
|
|
1699
|
+
// 播放控制图标(上一首/播放/暂停/下一首/停止):用 SVG 替代 ⏮▶⏸⏭⏹ 文本字形。
|
|
1700
|
+
// 这些 Unicode 符号(尤其 ⏸ 常以 emoji 呈现)宽高/基线不一致,点击切换会让按钮
|
|
1701
|
+
// 大小与位置偏移;统一用同尺寸 viewBox=24 的 SVG,保证按钮恒定尺寸、图标精确居中。
|
|
1702
|
+
const iconSvg = (path, w = 14) => (props) => React.createElement('svg', { className: props.className || '', width: w, height: w, viewBox: '0 0 24 24', fill: 'currentColor', 'aria-hidden': true },
|
|
1703
|
+
React.createElement('path', { d: path }));
|
|
1704
|
+
const PlayIcon = iconSvg('M8 5v14l11-7z');
|
|
1705
|
+
const PauseIcon = iconSvg('M6 19h4V5H6v14zm8-14v14h4V5h-4z');
|
|
1706
|
+
const PrevIcon = iconSvg('M6 6h2v12H6zm3.5 6l8.5 6V6z');
|
|
1707
|
+
const NextIcon = iconSvg('M6 18l8.5-6L6 6v12zM16 6h2v12h-2z');
|
|
1708
|
+
const StopIcon = iconSvg('M6 6h12v12H6z');
|
|
1699
1709
|
|
|
1700
1710
|
// ---- components ----
|
|
1701
1711
|
// Custom vertical volume slider. The native <input type=range> cannot be
|
|
@@ -1874,10 +1884,10 @@ window.__ModuleLoader__.load({
|
|
|
1874
1884
|
: React.createElement('span', { className: 'dsh-music-bar-time' }, fmtTime(s.position) + ' / ' + fmtTime(s.duration)))
|
|
1875
1885
|
: null,
|
|
1876
1886
|
heartBtn,
|
|
1877
|
-
hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: isBook ? '上一章' : '上一首', onClick: () => (isBook ? stepBook(-1) : step(-1)) },
|
|
1878
|
-
hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: '播放/暂停', onClick: togglePlay }, s.playing ?
|
|
1879
|
-
hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: isBook ? '下一章' : '下一首', onClick: () => (isBook ? stepBook(1) : step(1)) },
|
|
1880
|
-
hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: '停止', onClick: stop },
|
|
1887
|
+
hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: isBook ? '上一章' : '上一首', onClick: () => (isBook ? stepBook(-1) : step(-1)) }, React.createElement(PrevIcon, null)) : null,
|
|
1888
|
+
hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: '播放/暂停', onClick: togglePlay }, React.createElement(s.playing ? PauseIcon : PlayIcon, null)) : null,
|
|
1889
|
+
hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: isBook ? '下一章' : '下一首', onClick: () => (isBook ? stepBook(1) : step(1)) }, React.createElement(NextIcon, null)) : null,
|
|
1890
|
+
hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: '停止', onClick: stop }, React.createElement(StopIcon, null)) : null,
|
|
1881
1891
|
// 章节目录按钮:仅讲书(book)时出现,点击弹出章节列表并可跳章。
|
|
1882
1892
|
// 与音量/播放模式按钮同款圆形样式(dsh-music-mode-trigger);弹层用
|
|
1883
1893
|
// 与音量/播放模式同款的「相对容器 + 绝对定位」锚在按钮正上方。
|
|
@@ -2096,6 +2106,15 @@ window.__ModuleLoader__.load({
|
|
|
2096
2106
|
document.addEventListener('mousedown', onDown);
|
|
2097
2107
|
return () => document.removeEventListener('mousedown', onDown);
|
|
2098
2108
|
}, [qqJoin]);
|
|
2109
|
+
// QQ 播放列表层:进入或切换播放曲目时,把正在播放的那一行滚动到可见位置。
|
|
2110
|
+
useEffect(() => {
|
|
2111
|
+
const list = qqPlRef.current;
|
|
2112
|
+
if (list === null) return;
|
|
2113
|
+
const active = list.querySelector('.dsh-music-track-row.active');
|
|
2114
|
+
if (active !== null && typeof active.scrollIntoView === 'function') {
|
|
2115
|
+
active.scrollIntoView({ block: 'nearest' });
|
|
2116
|
+
}
|
|
2117
|
+
}, [layer, s.currentId, activePl]);
|
|
2099
2118
|
// 推荐歌单加载更多:热门推荐 12 条后用「全部分类」分页续载。
|
|
2100
2119
|
const [recPage, setRecPage] = useState(1);
|
|
2101
2120
|
const [recLoadingMore, setRecLoadingMore] = useState(false);
|
|
@@ -2111,6 +2130,10 @@ window.__ModuleLoader__.load({
|
|
|
2111
2130
|
const [topLoaded, setTopLoaded] = useState(false);
|
|
2112
2131
|
const [topDetail, setTopDetail] = useState(null); // { id, name, songs }
|
|
2113
2132
|
const [topLoading, setTopLoading] = useState(false);
|
|
2133
|
+
// 榜单详情分页:已加载歌曲总数 / 是否还有下一页 / 加载更多进行中。
|
|
2134
|
+
const [topTotal, setTopTotal] = useState(0);
|
|
2135
|
+
const [topHasMore, setTopHasMore] = useState(false);
|
|
2136
|
+
const [topLoadingMore, setTopLoadingMore] = useState(false);
|
|
2114
2137
|
const [newSongs, setNewSongs] = useState([]);
|
|
2115
2138
|
const [newLoaded, setNewLoaded] = useState(false);
|
|
2116
2139
|
// 登录。
|
|
@@ -2122,6 +2145,9 @@ window.__ModuleLoader__.load({
|
|
|
2122
2145
|
const pollRef = useRef(null);
|
|
2123
2146
|
const qrKeyRef = useRef('');
|
|
2124
2147
|
const loginModeRef = useRef(null);
|
|
2148
|
+
// QQ 播放列表层(layer='playlist')的滚动容器(.dsh-music-qq-body)引用,
|
|
2149
|
+
// 用于进入/切歌时把正在播放的曲目滚动到可见位置。
|
|
2150
|
+
const qqPlRef = useRef(null);
|
|
2125
2151
|
|
|
2126
2152
|
const json = (url) => jsonGet(url).catch(() => ({ ok: false, error: '网络错误' }));
|
|
2127
2153
|
// 搜索历史(localStorage 持久化,最近在前,最多 10 条)。
|
|
@@ -2209,6 +2235,22 @@ window.__ModuleLoader__.load({
|
|
|
2209
2235
|
setMineLoaded(true);
|
|
2210
2236
|
setBrowseLoading(false);
|
|
2211
2237
|
}
|
|
2238
|
+
// 删除自建歌单(DelPlaylist / PlaylistBaseWrite)。二次确认后调 Host,成功后本地移除。
|
|
2239
|
+
async function deleteMinePlaylist(pl) {
|
|
2240
|
+
const dirId = Number(pl && (pl.dirId || pl.tid || pl.id)) || 0;
|
|
2241
|
+
if (!dirId) { setBrowseErr('缺少歌单 dirId,无法删除'); return; }
|
|
2242
|
+
openConfirm('删除歌单', '确定删除 QQ 歌单「' + (pl.name || '') + '」?删除后不可恢复。', async () => {
|
|
2243
|
+
setBrowseLoading(true); setBrowseErr('');
|
|
2244
|
+
try {
|
|
2245
|
+
const d = await jsonPost('/dsh-music/qq/playlist-delete', { dirId });
|
|
2246
|
+
if (!d || !d.ok) throw new Error((d && d.error) || '删除歌单失败');
|
|
2247
|
+
setMinePlays((prev) => prev.filter((p) => String(p.id) !== String(pl.id)));
|
|
2248
|
+
} catch (err) {
|
|
2249
|
+
setBrowseErr(String((err && err.message) || err));
|
|
2250
|
+
}
|
|
2251
|
+
setBrowseLoading(false);
|
|
2252
|
+
}, '删除', true);
|
|
2253
|
+
}
|
|
2212
2254
|
async function loadCategory(cat) {
|
|
2213
2255
|
setCurCategory(cat); setCatPlays([]); setBrowseLoading(true); setBrowseErr('');
|
|
2214
2256
|
setCatPage(1); setCatHasMore(true);
|
|
@@ -2242,12 +2284,33 @@ window.__ModuleLoader__.load({
|
|
|
2242
2284
|
setTopLoaded(true);
|
|
2243
2285
|
setBrowseLoading(false);
|
|
2244
2286
|
}
|
|
2287
|
+
const TOP_PAGE = 30;
|
|
2245
2288
|
async function loadTopSongs(top) {
|
|
2246
2289
|
setTopDetail(null); setTopLoading(true); setBrowseErr('');
|
|
2247
|
-
|
|
2248
|
-
|
|
2290
|
+
setTopTotal(0); setTopHasMore(false);
|
|
2291
|
+
const d = await json('/dsh-music/qq/top-songs?topId=' + encodeURIComponent(top.id) + '&offset=0&num=' + TOP_PAGE);
|
|
2292
|
+
if (d && d.ok) {
|
|
2293
|
+
const t = d.toplist || {};
|
|
2294
|
+
setTopDetail({ ...t, id: top.id, name: t.name || top.name, cover: t.cover || top.cover || '' });
|
|
2295
|
+
setTopTotal(Number(t.total) || (t.songs || []).length);
|
|
2296
|
+
setTopHasMore(!!t.hasMore);
|
|
2297
|
+
} else setBrowseErr((d && d.error) || '加载榜单失败');
|
|
2249
2298
|
setTopLoading(false);
|
|
2250
2299
|
}
|
|
2300
|
+
// 榜单「加载更多」:从当前已加载数量继续取下一页并追加,更新 total/hasMore。
|
|
2301
|
+
async function loadMoreTopSongs() {
|
|
2302
|
+
if (topLoadingMore || !topDetail) return;
|
|
2303
|
+
setTopLoadingMore(true); setBrowseErr('');
|
|
2304
|
+
const offset = (topDetail.songs || []).length;
|
|
2305
|
+
const d = await json('/dsh-music/qq/top-songs?topId=' + encodeURIComponent(topDetail.id || topDetail.topId) + '&offset=' + offset + '&num=' + TOP_PAGE);
|
|
2306
|
+
if (d && d.ok) {
|
|
2307
|
+
const t = d.toplist || {};
|
|
2308
|
+
setTopDetail((cur) => (cur ? { ...cur, songs: [...(cur.songs || []), ...((t.songs || []))] } : cur));
|
|
2309
|
+
setTopTotal(Number(t.total) || offset + (t.songs || []).length);
|
|
2310
|
+
setTopHasMore(!!t.hasMore);
|
|
2311
|
+
} else setBrowseErr((d && d.error) || '加载更多失败');
|
|
2312
|
+
setTopLoadingMore(false);
|
|
2313
|
+
}
|
|
2251
2314
|
async function loadNewSongs() {
|
|
2252
2315
|
if (newLoaded) return;
|
|
2253
2316
|
setBrowseLoading(true); setBrowseErr('');
|
|
@@ -2256,11 +2319,15 @@ window.__ModuleLoader__.load({
|
|
|
2256
2319
|
setNewLoaded(true);
|
|
2257
2320
|
setBrowseLoading(false);
|
|
2258
2321
|
}
|
|
2259
|
-
function backToTops() { setTopDetail(null); }
|
|
2260
|
-
async function openPlaylist(pl) {
|
|
2322
|
+
function backToTops() { setTopDetail(null); setTopTotal(0); setTopHasMore(false); }
|
|
2323
|
+
async function openPlaylist(pl, mine) {
|
|
2261
2324
|
setActivePl(null); setLayer('playlist'); setPlLoading(true); saveUi('playlist', pl.id, pl.name);
|
|
2325
|
+
// _dirId:从歌单移除歌曲需要 QQ 歌单的目录 id(「我的歌单」卡片带 dirId);
|
|
2326
|
+
// mine 标记这是「我自己的歌单」(含「我喜欢」),详情里才允许用「−」从该歌单移除。
|
|
2327
|
+
const _dirId = Number((pl && (pl.dirId || pl.tid || pl.id))) || 0;
|
|
2262
2328
|
const d = await json('/dsh-music/qq/playlist/' + encodeURIComponent(pl.id));
|
|
2263
|
-
if (d && d.ok) setActivePl({ ...d.playlist, source: pl.name || '歌单'
|
|
2329
|
+
if (d && d.ok) setActivePl({ ...d.playlist, source: pl.name || '歌单', mine: !!mine, _dirId });
|
|
2330
|
+
else setBrowseErr((d && d.error) || '加载失败');
|
|
2264
2331
|
setPlLoading(false);
|
|
2265
2332
|
}
|
|
2266
2333
|
function openQueue() {
|
|
@@ -2372,7 +2439,7 @@ window.__ModuleLoader__.load({
|
|
|
2372
2439
|
|
|
2373
2440
|
// ---- 渲染辅助 ----
|
|
2374
2441
|
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); };
|
|
2375
|
-
const songRow = (song, queue, sourceLabel) => {
|
|
2442
|
+
const songRow = (song, queue, sourceLabel, inMine) => {
|
|
2376
2443
|
const id = String(song.songmid || song.id);
|
|
2377
2444
|
const active = s.currentId === 'qq:' + id;
|
|
2378
2445
|
const playing = active && s.playing;
|
|
@@ -2387,12 +2454,38 @@ window.__ModuleLoader__.load({
|
|
|
2387
2454
|
React.createElement('span', { className: 'dsh-music-track-title' }, (playing ? '▶ ' : '') + song.title),
|
|
2388
2455
|
vip ? React.createElement('span', { className: 'dsh-music-online-tag vip' }, 'VIP') : null),
|
|
2389
2456
|
React.createElement('span', { className: 'dsh-music-online-tag' }, artists || 'QQ')),
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2457
|
+
// 我的歌单详情:歌曲已在该歌单 → 显示「−」从该歌单移除;其它场景显示「+」加入歌单。
|
|
2458
|
+
inMine
|
|
2459
|
+
? React.createElement('button', {
|
|
2460
|
+
className: 'dsh-music-playlist-mini remove',
|
|
2461
|
+
title: '从「' + ((activePl && activePl.name) || '当前歌单') + '」移除',
|
|
2462
|
+
onClick: (e) => { e.stopPropagation(); removeFromActivePlaylist(song); },
|
|
2463
|
+
}, '−')
|
|
2464
|
+
: React.createElement('button', {
|
|
2465
|
+
className: 'dsh-music-playlist-mini add',
|
|
2466
|
+
title: '加入我的歌单',
|
|
2467
|
+
onClick: (e) => { e.stopPropagation(); openQqJoin(song, e); },
|
|
2468
|
+
}, '+'));
|
|
2395
2469
|
};
|
|
2470
|
+
// 从「我的歌单」当前歌单移除歌曲(DelSonglist);「我喜欢」(dirId=201) 等同取消收藏。
|
|
2471
|
+
async function removeFromActivePlaylist(song) {
|
|
2472
|
+
const pl = activePl;
|
|
2473
|
+
if (!pl) return;
|
|
2474
|
+
const id = String(song.songmid || song.id);
|
|
2475
|
+
const dirId = Number(pl._dirId || pl.dirId || pl.id) || 0;
|
|
2476
|
+
try {
|
|
2477
|
+
const d = await jsonPost('/dsh-music/qq/playlist-remove', { song, dirId, tid: 0 });
|
|
2478
|
+
if (!d || !d.ok) throw new Error((d && d.error) || '从歌单移除失败');
|
|
2479
|
+
// 就地移除该行,并刷新「我的歌单」数目。
|
|
2480
|
+
setActivePl((cur) => (cur ? { ...cur, songs: (cur.songs || []).filter((t) => String(t.songmid || t.id) !== id) } : cur));
|
|
2481
|
+
loadMine();
|
|
2482
|
+
// 「我喜欢」:同步收藏集合并把当前播放的这首歌标记为未收藏。
|
|
2483
|
+
if (dirId === 201) {
|
|
2484
|
+
refreshQQFavIds();
|
|
2485
|
+
if (String(s.currentId) === 'qq:' + id) set({ qqFaved: false });
|
|
2486
|
+
}
|
|
2487
|
+
} catch (err) { setQError(String((err && err.message) || err)); }
|
|
2488
|
+
}
|
|
2396
2489
|
// 「加入歌单」:打开弹窗(列出我的歌单),并把歌曲加入所选歌单 / 新建歌单。
|
|
2397
2490
|
function openQqJoin(song, e) {
|
|
2398
2491
|
const r = e.currentTarget.getBoundingClientRect();
|
|
@@ -2451,12 +2544,16 @@ window.__ModuleLoader__.load({
|
|
|
2451
2544
|
React.createElement('button', { className: 'dsh-music-add-pop-item new', onClick: qqJoinCreate }, '+ 新建歌单'),
|
|
2452
2545
|
);
|
|
2453
2546
|
})()) : null;
|
|
2454
|
-
const playRow = (pl) => {
|
|
2547
|
+
const playRow = (pl, mine) => {
|
|
2548
|
+
// 注意:map((pl) => playRow(pl)) 会把数组下标作为第二个参数传入(Array#map 传
|
|
2549
|
+
// (element, index, array))。这里必须用严格 true 判断「我的歌单」,否则推荐/分类/
|
|
2550
|
+
// 搜索等来源里除第一项外的卡片会把下标当真值,误显示删除按钮。
|
|
2551
|
+
const isMine = mine === true;
|
|
2455
2552
|
const meta = (pl.trackCount > 0 ? pl.trackCount + ' 首' : '')
|
|
2456
2553
|
+ (pl.playCount ? ' · 播放 ' + fmtCount(pl.playCount) : '');
|
|
2457
|
-
|
|
2554
|
+
const card = React.createElement('button', {
|
|
2458
2555
|
key: pl.id, className: 'dsh-music-playlist-card', title: pl.name + ' - ' + (pl.creator || ''),
|
|
2459
|
-
onClick: () => openPlaylist(pl),
|
|
2556
|
+
onClick: () => openPlaylist(pl, isMine),
|
|
2460
2557
|
},
|
|
2461
2558
|
React.createElement('img', {
|
|
2462
2559
|
className: 'dsh-music-playlist-cover',
|
|
@@ -2467,6 +2564,16 @@ window.__ModuleLoader__.load({
|
|
|
2467
2564
|
React.createElement('span', { className: 'dsh-music-playlist-name' }, pl.name),
|
|
2468
2565
|
React.createElement('span', { className: 'dsh-music-playlist-meta' },
|
|
2469
2566
|
(pl.creator ? pl.creator + ' · ' : '') + meta)));
|
|
2567
|
+
// 「我的歌单」卡片:右上角提供删除按钮(仅本人创建的歌单,「我喜欢」dirId=201 除外)。
|
|
2568
|
+
if (!isMine) return card;
|
|
2569
|
+
const dirId = Number(pl && (pl.dirId || pl.tid || pl.id)) || 0;
|
|
2570
|
+
const deletable = dirId !== 0 && dirId !== 201;
|
|
2571
|
+
return React.createElement('div', { key: pl.id, className: 'dsh-music-qq-mine-card' },
|
|
2572
|
+
card,
|
|
2573
|
+
deletable ? React.createElement('button', {
|
|
2574
|
+
className: 'dsh-music-qq-mine-del', title: '删除歌单「' + pl.name + '」',
|
|
2575
|
+
onClick: (e) => { e.stopPropagation(); deleteMinePlaylist(pl); },
|
|
2576
|
+
}, '✕') : null);
|
|
2470
2577
|
};
|
|
2471
2578
|
const catTab = (cat) => React.createElement('button', {
|
|
2472
2579
|
key: cat.id, className: 'dsh-music-qq-cat' + (curCategory && curCategory.id === cat.id ? ' active' : ''),
|
|
@@ -2561,11 +2668,11 @@ window.__ModuleLoader__.load({
|
|
|
2561
2668
|
React.createElement('span', { className: 'dsh-music-settings-cur', title: pl.name }, pl.source === '歌单' ? '▸ 歌单:' + pl.name : pl.name),
|
|
2562
2669
|
React.createElement('span', { className: 'dsh-music-hint' }, (pl.creator ? (pl.creator + ' · ') : '') + ((pl.songs || []).length + ' 首'))),
|
|
2563
2670
|
pl.description ? React.createElement('p', { className: 'dsh-music-hint' }, pl.description) : null),
|
|
2564
|
-
React.createElement('div', { className: 'dsh-music-qq-body' },
|
|
2671
|
+
React.createElement('div', { className: 'dsh-music-qq-body', ref: qqPlRef },
|
|
2565
2672
|
plLoading
|
|
2566
2673
|
? React.createElement('div', { className: 'dsh-music-hint' }, '加载中…')
|
|
2567
2674
|
: (pl.songs && pl.songs.length
|
|
2568
|
-
? React.createElement('div', null, pl.songs.map((song) => songRow(song, pl.songs, pl.name)))
|
|
2675
|
+
? React.createElement('div', null, pl.songs.map((song) => songRow(song, pl.songs, pl.name, pl.mine)))
|
|
2569
2676
|
: React.createElement('div', { className: 'dsh-music-hint' }, '暂无歌曲。'))),
|
|
2570
2677
|
loginModal,
|
|
2571
2678
|
qqJoinMenu);
|
|
@@ -2599,12 +2706,12 @@ window.__ModuleLoader__.load({
|
|
|
2599
2706
|
resultTabBtn('songs', '歌曲'),
|
|
2600
2707
|
resultTabBtn('playlists', '相关歌单')),
|
|
2601
2708
|
resultTab === 'playlists'
|
|
2602
|
-
? React.createElement('div', null, plResults.map(playRow), plMoreBtn)
|
|
2709
|
+
? React.createElement('div', null, plResults.map((p) => playRow(p)), plMoreBtn)
|
|
2603
2710
|
: React.createElement('div', null, results.map((song) => songRow(song, results, '搜索结果')), songMoreBtn));
|
|
2604
2711
|
} else if (hasSongs) {
|
|
2605
2712
|
resultContent = React.createElement('div', null, results.map((song) => songRow(song, results, '搜索结果')), songMoreBtn);
|
|
2606
2713
|
} else {
|
|
2607
|
-
resultContent = React.createElement('div', null, plResults.map(playRow), plMoreBtn);
|
|
2714
|
+
resultContent = React.createElement('div', null, plResults.map((p) => playRow(p)), plMoreBtn);
|
|
2608
2715
|
}
|
|
2609
2716
|
body = React.createElement('div', null, searchBox,
|
|
2610
2717
|
searched ? React.createElement('div', null, resultContent) : null);
|
|
@@ -2614,7 +2721,7 @@ window.__ModuleLoader__.load({
|
|
|
2614
2721
|
const listEl = !mineLoaded
|
|
2615
2722
|
? React.createElement('div', { className: 'dsh-music-hint' }, '加载我的歌单…')
|
|
2616
2723
|
: (minePlays.length > 0
|
|
2617
|
-
? minePlays.map(playRow)
|
|
2724
|
+
? minePlays.map((p) => playRow(p, true))
|
|
2618
2725
|
: React.createElement('div', { className: 'dsh-music-hint' }, '暂无歌单。可到 QQ 音乐 App 创建或收藏歌单后再来查看。'));
|
|
2619
2726
|
content = React.createElement('div', null, listEl);
|
|
2620
2727
|
body = React.createElement('div', null, content,
|
|
@@ -2636,7 +2743,7 @@ window.__ModuleLoader__.load({
|
|
|
2636
2743
|
React.createElement('div', { className: 'dsh-music-qq-cats' }, shownCats.length ? shownCats.map(catTab) : React.createElement('span', { className: 'dsh-music-hint' }, '加载分类中…')),
|
|
2637
2744
|
catToggle,
|
|
2638
2745
|
curCategory ? React.createElement('div', null,
|
|
2639
|
-
(catPlays.length ? catPlays.map(playRow) : React.createElement('div', { className: 'dsh-music-hint' }, '该分类暂无歌单。')),
|
|
2746
|
+
(catPlays.length ? catPlays.map((p) => playRow(p)) : React.createElement('div', { className: 'dsh-music-hint' }, '该分类暂无歌单。')),
|
|
2640
2747
|
catMoreBtn) : null);
|
|
2641
2748
|
body = React.createElement('div', null, content,
|
|
2642
2749
|
browseErr ? React.createElement('div', { className: 'dsh-music-error' }, browseErr) : null,
|
|
@@ -2644,10 +2751,15 @@ window.__ModuleLoader__.load({
|
|
|
2644
2751
|
} else if (browseTab === 'tops') {
|
|
2645
2752
|
let content;
|
|
2646
2753
|
if (topDetail) {
|
|
2647
|
-
// 榜单详情:返回 +
|
|
2754
|
+
// 榜单详情:返回 + 歌曲列表(支持「加载更多」分页续载)。
|
|
2648
2755
|
const rows = topDetail.songs && topDetail.songs.length
|
|
2649
2756
|
? topDetail.songs.map((song) => songRow(song, topDetail.songs, topDetail.name))
|
|
2650
2757
|
: React.createElement('div', { className: 'dsh-music-hint' }, '该榜单暂无歌曲。');
|
|
2758
|
+
const moreBtn = topHasMore
|
|
2759
|
+
? React.createElement('div', { className: 'dsh-music-qq-loadmore' },
|
|
2760
|
+
React.createElement('button', { className: 'dsh-music-qq-loadmore-btn', onClick: loadMoreTopSongs },
|
|
2761
|
+
topLoadingMore ? '加载中…' : '加载更多'))
|
|
2762
|
+
: null;
|
|
2651
2763
|
content = React.createElement('div', null,
|
|
2652
2764
|
React.createElement('button', { className: 'dsh-music-settings-btn ghost', onClick: backToTops }, '← 返回'),
|
|
2653
2765
|
React.createElement('div', { className: 'dsh-music-qq-topdetail-head' },
|
|
@@ -2657,8 +2769,11 @@ window.__ModuleLoader__.load({
|
|
|
2657
2769
|
}) : null,
|
|
2658
2770
|
React.createElement('div', null,
|
|
2659
2771
|
React.createElement('div', { className: 'dsh-music-playlist-name' }, topDetail.name),
|
|
2660
|
-
React.createElement('div', { className: 'dsh-music-hint' },
|
|
2661
|
-
|
|
2772
|
+
React.createElement('div', { className: 'dsh-music-hint' },
|
|
2773
|
+
(topDetail.updateTime ? '更新于 ' + topDetail.updateTime + ' · ' : '')
|
|
2774
|
+
+ (topTotal ? (topDetail.songs || []).length + ' / ' + topTotal + ' 首' : '')))),
|
|
2775
|
+
rows,
|
|
2776
|
+
moreBtn);
|
|
2662
2777
|
} else {
|
|
2663
2778
|
const topCards = (tl) => tl.map((t) => {
|
|
2664
2779
|
const meta = t.listenNum ? ((t.listenNum / 1e4).toFixed(0) + '万收听') : (t.totalNum ? t.totalNum + ' 首' : '');
|
|
@@ -2695,7 +2810,7 @@ window.__ModuleLoader__.load({
|
|
|
2695
2810
|
browseErr ? React.createElement('div', { className: 'dsh-music-error' }, browseErr) : null,
|
|
2696
2811
|
browseLoading ? React.createElement('div', { className: 'dsh-music-loading' }, '加载中…') : null);
|
|
2697
2812
|
} else {
|
|
2698
|
-
const recCards = recommended.length > 0 ? recommended.map(playRow) : React.createElement('div', { className: 'dsh-music-hint' }, '加载推荐歌单…');
|
|
2813
|
+
const recCards = recommended.length > 0 ? recommended.map((p) => playRow(p)) : React.createElement('div', { className: 'dsh-music-hint' }, '加载推荐歌单…');
|
|
2699
2814
|
const moreBtn = recommended.length > 0 && recHasMore
|
|
2700
2815
|
? React.createElement('div', { className: 'dsh-music-qq-loadmore' },
|
|
2701
2816
|
React.createElement('button', { className: 'dsh-music-qq-loadmore-btn', onClick: loadMoreRecommended },
|
|
@@ -2710,7 +2825,7 @@ window.__ModuleLoader__.load({
|
|
|
2710
2825
|
|
|
2711
2826
|
const head = React.createElement('div', { className: 'dsh-music-qq-head' },
|
|
2712
2827
|
React.createElement('div', { className: 'dsh-music-qq-toolbar' },
|
|
2713
|
-
React.createElement('button', { className: 'dsh-music-settings-btn ghost', onClick: openQueue }, '
|
|
2828
|
+
React.createElement('button', { className: 'dsh-music-settings-btn ghost', onClick: openQueue }, '播放列表'),
|
|
2714
2829
|
React.createElement('button', { className: 'dsh-music-settings-btn ghost', onClick: logout }, '退出登录')),
|
|
2715
2830
|
React.createElement('div', { className: 'dsh-music-qq-viewtabs' },
|
|
2716
2831
|
browseTabBtn('mine', '我的歌单'),
|
|
@@ -3446,7 +3561,9 @@ window.__ModuleLoader__.load({
|
|
|
3446
3561
|
'.dsh-music-list-body { flex: 1; min-height: 0; display: flex; flex-direction: column; }\n' +
|
|
3447
3562
|
'.dsh-music-track { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; padding: 6px 8px; border: none; background: transparent; border-radius: 6px; color: var(--dsw-alias-label-primary, #e6e6e6); cursor: pointer; font-size: 12px; }\n' +
|
|
3448
3563
|
'.dsh-music-track:hover { background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.06)); }\n' +
|
|
3449
|
-
|
|
3564
|
+
// 正在播放/选中的条目:填充强调色底 + 强调色文字,让当前条目一眼可见(选中态)。
|
|
3565
|
+
'.dsh-music-track.active { color: var(--dsh-music-accent, #2f9e6e); background: color-mix(in srgb, var(--dsh-music-accent, #2f9e6e) 14%, transparent); }\n' +
|
|
3566
|
+
'.dsh-music-track.active .dsh-music-track-name { font-weight: 600; }\n' +
|
|
3450
3567
|
'.dsh-music-track-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n' +
|
|
3451
3568
|
// 在线 QQ 歌曲行:歌名 + 内嵌 VIP 徽标并排,歌名省略、VIP 徽标不省略。
|
|
3452
3569
|
'.dsh-music-track-name.qq { display: inline-flex; align-items: center; gap: 5px; overflow: hidden; }\n' +
|
|
@@ -3455,6 +3572,11 @@ window.__ModuleLoader__.load({
|
|
|
3455
3572
|
// 歌单卡片:封面图 + 名称 + 元信息,网格排布。
|
|
3456
3573
|
'.dsh-music-playlist-card { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; padding: 8px; border: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,0.25)); background: var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.06)); border-radius: 10px; color: var(--dsw-alias-label-primary, #e6e6e6); cursor: pointer; font-size: 12px; }\n' +
|
|
3457
3574
|
'.dsh-music-playlist-card:hover { border-color: var(--dsh-music-accent, #2f9e6e); background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.05)); }\n' +
|
|
3575
|
+
// 「我的歌单」卡片:外层相对定位,右上角删除按钮悬浮。
|
|
3576
|
+
'.dsh-music-qq-mine-card { position: relative; }\n' +
|
|
3577
|
+
'.dsh-music-qq-mine-del { position: absolute; top: 6px; right: 6px; z-index: 2; width: 20px; height: 20px; line-height: 18px; padding: 0; text-align: center; border-radius: 50%; border: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,0.35)); background: var(--dsw-alias-bg-overlay, rgba(0,0,0,0.55)); color: var(--dsw-alias-label-secondary, #8a8f98); cursor: pointer; font-size: 11px; opacity: 0; transition: opacity 0.15s; }\n' +
|
|
3578
|
+
'.dsh-music-qq-mine-card:hover .dsh-music-qq-mine-del { opacity: 1; }\n' +
|
|
3579
|
+
'.dsh-music-qq-mine-del:hover { color: #fff; background: #c9352c; border-color: #c9352c; }\n' +
|
|
3458
3580
|
'.dsh-music-playlist-cover { width: 56px; height: 56px; border-radius: 8px; object-fit: cover; flex: 0 0 auto; background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.06)); }\n' +
|
|
3459
3581
|
'.dsh-music-playlist-info { display: flex; flex-direction: column; gap: 4px; min-width: 0; }\n' +
|
|
3460
3582
|
'.dsh-music-playlist-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }\n' +
|
|
@@ -3570,7 +3692,8 @@ window.__ModuleLoader__.load({
|
|
|
3570
3692
|
'.dsh-music-toc-list { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }\n' +
|
|
3571
3693
|
'.dsh-music-toc-item { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; padding: 5px 8px; border: none; background: transparent; border-radius: 6px; color: var(--dsw-alias-label-primary, #e6e6e6); cursor: pointer; font-size: 12px; }\n' +
|
|
3572
3694
|
'.dsh-music-toc-item:hover { background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.06)); }\n' +
|
|
3573
|
-
'.dsh-music-toc-item.active { color: var(--dsh-music-accent, #2f9e6e); }\n' +
|
|
3695
|
+
'.dsh-music-toc-item.active { color: var(--dsh-music-accent, #2f9e6e); background: color-mix(in srgb, var(--dsh-music-accent, #2f9e6e) 14%, transparent); }\n' +
|
|
3696
|
+
'.dsh-music-toc-item.active .dsh-music-toc-heading { font-weight: 600; }\n' +
|
|
3574
3697
|
'.dsh-music-toc-type { flex: none; font-size: 10px; padding: 1px 5px; border-radius: 4px; background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.08)); color: var(--dsw-alias-label-secondary, #8a8f98); }\n' +
|
|
3575
3698
|
'.dsh-music-toc-item.active .dsh-music-toc-type { background: var(--dsh-music-accent, #2f9e6e); color: var(--dsh-music-accent-fg, #fff); }\n' +
|
|
3576
3699
|
'.dsh-music-toc-heading { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n' +
|
|
@@ -3588,7 +3711,9 @@ window.__ModuleLoader__.load({
|
|
|
3588
3711
|
'.dsh-music-playlist-missing { flex: none; margin-left: auto; font-size: 11px; color: var(--dsw-alias-state-warn-primary, #d9a441); }\n' +
|
|
3589
3712
|
'.dsh-music-playlist-row { display: flex; align-items: center; gap: 4px; }\n' +
|
|
3590
3713
|
'.dsh-music-playlist-row .dsh-music-track { flex: 1; min-width: 0; }\n' +
|
|
3591
|
-
'.dsh-music-playlist-row.active
|
|
3714
|
+
'.dsh-music-playlist-row.active { border-radius: 6px; background: color-mix(in srgb, var(--dsh-music-accent, #2f9e6e) 14%, transparent); }\n' +
|
|
3715
|
+
'.dsh-music-playlist-row.active .dsh-music-track { color: var(--dsh-music-accent, #2f9e6e); background: transparent; }\n' +
|
|
3716
|
+
'.dsh-music-playlist-row.active .dsh-music-track-name { font-weight: 600; }\n' +
|
|
3592
3717
|
'.dsh-music-playlist-mini { flex: none; width: 20px; height: 20px; padding: 0; border: none; background: transparent; border-radius: 4px; color: var(--dsw-alias-label-secondary, #8a8f98); cursor: pointer; font-size: 12px; line-height: 1; }\n' +
|
|
3593
3718
|
'.dsh-music-playlist-mini:hover { background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.06)); color: var(--dsh-music-accent, #2f9e6e); }\n' +
|
|
3594
3719
|
'.dsh-music-playlist-mini.del:hover { color: var(--dsw-alias-state-error-primary, #e5534b); }\n' +
|
|
@@ -3601,8 +3726,12 @@ window.__ModuleLoader__.load({
|
|
|
3601
3726
|
// 曲库每行:track 按钮 + 行尾「+」(加入歌单)
|
|
3602
3727
|
'.dsh-music-track-row { display: flex; align-items: center; gap: 4px; }\n' +
|
|
3603
3728
|
'.dsh-music-track-row .dsh-music-track { flex: 1; min-width: 0; }\n' +
|
|
3604
|
-
'.dsh-music-track-row.active
|
|
3729
|
+
'.dsh-music-track-row.active { border-radius: 6px; background: color-mix(in srgb, var(--dsh-music-accent, #2f9e6e) 14%, transparent); }\n' +
|
|
3730
|
+
'.dsh-music-track-row.active .dsh-music-track { color: var(--dsh-music-accent, #2f9e6e); background: transparent; }\n' +
|
|
3731
|
+
'.dsh-music-track-row.active .dsh-music-track-name { font-weight: 600; }\n' +
|
|
3605
3732
|
'.dsh-music-playlist-mini.add { color: var(--dsh-music-accent, #2f9e6e); }\n' +
|
|
3733
|
+
'.dsh-music-playlist-mini.remove { color: var(--dsh-music-accent, #2f9e6e); }\n' +
|
|
3734
|
+
'.dsh-music-playlist-mini.remove:hover { color: var(--dsw-alias-state-error-primary, #e5534b); }\n' +
|
|
3606
3735
|
'.dsh-music-add-pop { position: fixed; z-index: 1200; min-width: 150px; max-width: 210px; display: flex; flex-direction: column; gap: 2px; padding: 6px; background: var(--dsw-alias-bg-overlay, #1e1f22); border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.35)); border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.3); }\n' +
|
|
3607
3736
|
'.dsh-music-add-pop-item { display: block; width: 100%; text-align: left; padding: 5px 8px; border: none; background: transparent; border-radius: 6px; color: var(--dsw-alias-label-primary, #e6e6e6); cursor: pointer; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n' +
|
|
3608
3737
|
'.dsh-music-add-pop-item:hover { background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.06)); color: var(--dsh-music-accent, #2f9e6e); }\n' +
|
package/lib/index.js
CHANGED
|
@@ -1698,6 +1698,19 @@ export function apply(ctx) {
|
|
|
1698
1698
|
} catch (err) { writeJson(res, { ok: false, error: String((err && err.message) || err) }, 502) }
|
|
1699
1699
|
return
|
|
1700
1700
|
}
|
|
1701
|
+
// 从歌单移除歌曲(DelSonglist);「我喜欢」也是 dirId=201,同在列。
|
|
1702
|
+
if (pathname === '/dsh-music/qq/playlist-remove' && req.method === 'POST') {
|
|
1703
|
+
const body = await readBody(req)
|
|
1704
|
+
const song = (body && body.song) || {}
|
|
1705
|
+
const dirId = Number((body && body.dirId)) || 0
|
|
1706
|
+
const tid = Number((body && body.tid)) || 0
|
|
1707
|
+
await loadQQCookie()
|
|
1708
|
+
try {
|
|
1709
|
+
const ok = await QQ.deleteSongFromPlaylist(song, dirId, tid, qq.cookie)
|
|
1710
|
+
writeJson(res, { ok })
|
|
1711
|
+
} catch (err) { writeJson(res, { ok: false, error: String((err && err.message) || err) }, 502) }
|
|
1712
|
+
return
|
|
1713
|
+
}
|
|
1701
1714
|
// 创建自建歌单(DirCreate),返回新歌单 id/name。
|
|
1702
1715
|
if (pathname === '/dsh-music/qq/playlist-create' && req.method === 'POST') {
|
|
1703
1716
|
const body = await readBody(req)
|
|
@@ -1710,6 +1723,18 @@ export function apply(ctx) {
|
|
|
1710
1723
|
} catch (err) { writeJson(res, { ok: false, error: String((err && err.message) || err) }, 502) }
|
|
1711
1724
|
return
|
|
1712
1725
|
}
|
|
1726
|
+
// 删除自建歌单(DelPlaylist / PlaylistBaseWrite)。仅限本人创建的歌单;「我喜欢」不可删。
|
|
1727
|
+
if (pathname === '/dsh-music/qq/playlist-delete' && req.method === 'POST') {
|
|
1728
|
+
const body = await readBody(req)
|
|
1729
|
+
const dirId = Number((body && body.dirId)) || 0
|
|
1730
|
+
if (!dirId) { writeJson(res, { ok: false, error: '缺少歌单 dirId' }, 400); return }
|
|
1731
|
+
await loadQQCookie()
|
|
1732
|
+
try {
|
|
1733
|
+
const ok = await QQ.deletePlaylist(dirId, qq.cookie)
|
|
1734
|
+
writeJson(res, { ok })
|
|
1735
|
+
} catch (err) { writeJson(res, { ok: false, error: String((err && err.message) || err) }, 502) }
|
|
1736
|
+
return
|
|
1737
|
+
}
|
|
1713
1738
|
// 获取「我喜欢」已收藏歌曲的 songid + songmid 列表(供播放时判断当前曲目是否已收藏)。
|
|
1714
1739
|
if (pathname === '/dsh-music/qq/liked' && req.method === 'GET') {
|
|
1715
1740
|
await loadQQCookie()
|
|
@@ -1739,8 +1764,10 @@ export function apply(ctx) {
|
|
|
1739
1764
|
if (pathname === '/dsh-music/qq/top-songs' && req.method === 'GET') {
|
|
1740
1765
|
const topId = (url.searchParams.get('topId') || '').trim()
|
|
1741
1766
|
if (!/^[0-9]+$/.test(topId)) { writeJson(res, { ok: false, error: 'bad topId' }, 400); return }
|
|
1767
|
+
const offset = parseInt(url.searchParams.get('offset') || '0', 10)
|
|
1768
|
+
const num = parseInt(url.searchParams.get('num') || '30', 10)
|
|
1742
1769
|
await loadQQCookie()
|
|
1743
|
-
try { writeJson(res, { ok: true, toplist: await QQ.getTopListSongs(topId, qq.cookie) }) }
|
|
1770
|
+
try { writeJson(res, { ok: true, toplist: await QQ.getTopListSongs(topId, qq.cookie, Number.isFinite(offset) ? offset : 0, Number.isFinite(num) ? num : 30) }) }
|
|
1744
1771
|
catch (err) { writeJson(res, { ok: false, error: String((err && err.message) || err) }, 502) }
|
|
1745
1772
|
return
|
|
1746
1773
|
}
|
package/lib/qq.js
CHANGED
|
@@ -613,6 +613,20 @@ export async function addSongToPlaylist(song, dirId, tid = 0, cookie = '') {
|
|
|
613
613
|
return true
|
|
614
614
|
}
|
|
615
615
|
|
|
616
|
+
// 把歌曲从某个歌单移除(DelSonglist)。「我喜欢」也是 dirId=201,同在列。
|
|
617
|
+
// tid 固定 0 即可(与 addSongToPlaylist 一致)。
|
|
618
|
+
export async function deleteSongFromPlaylist(song, dirId, tid = 0, cookie = '') {
|
|
619
|
+
const songId = Number(song.songid) || 0
|
|
620
|
+
if (!songId) throw new Error('缺少歌曲 songid,无法从歌单移除')
|
|
621
|
+
await playlistDetailWrite('DelSonglist', {
|
|
622
|
+
dirId: Number(dirId) || 0,
|
|
623
|
+
tid: Number(tid) || 0,
|
|
624
|
+
bFmtUtf8: true,
|
|
625
|
+
v_songInfo: [{ songId, songType: Number(song.songtype) || 0 }],
|
|
626
|
+
}, cookie)
|
|
627
|
+
return true
|
|
628
|
+
}
|
|
629
|
+
|
|
616
630
|
// 创建自建歌单(AddPlaylist / PlaylistBaseWrite),返回 { id, name }。
|
|
617
631
|
// AddPlaylist 响应把新歌单信息嵌套在 data.result 下(dirId/dirName/tid)。
|
|
618
632
|
export async function createPlaylist(name, cookie = '') {
|
|
@@ -624,6 +638,17 @@ export async function createPlaylist(name, cookie = '') {
|
|
|
624
638
|
return { id, name: dec(result.dirName || result.dirname || result.name || dirName) }
|
|
625
639
|
}
|
|
626
640
|
|
|
641
|
+
// 删除自建歌单(DelPlaylist / PlaylistBaseWrite)。dirId = 该歌单的目录 id。
|
|
642
|
+
// 只能删除本人创建的歌单;「我喜欢」(dirId=201)不允许删除,由前端隐藏入口。
|
|
643
|
+
// DelPlaylist 响应把结果嵌套在 data.result 下;成功返回 true,失败抛错。
|
|
644
|
+
export async function deletePlaylist(dirId, cookie = '') {
|
|
645
|
+
const id = Number(dirId) || 0
|
|
646
|
+
if (!id) throw new Error('缺少歌单 dirId,无法删除')
|
|
647
|
+
if (id === 201) throw new Error('「我喜欢」不可删除')
|
|
648
|
+
await playlistDetailWrite('DelPlaylist', { dirId: id }, cookie, 'music.musicasset.PlaylistBaseWrite')
|
|
649
|
+
return true
|
|
650
|
+
}
|
|
651
|
+
|
|
627
652
|
// 获取「我喜欢」(dirid=201)里已收藏歌曲的 songid + songmid 集合,用于播放时判断当前曲目是否已收藏。
|
|
628
653
|
// 返回 { ids: number[], mids: string[] }——songmid 是稳定字符串标识,避免 songid 大整数/类型不一致导致匹配失败。
|
|
629
654
|
export async function getQQFavIds(cookie = '') {
|
|
@@ -705,31 +730,39 @@ export async function getTopLists(cookie = '') {
|
|
|
705
730
|
}
|
|
706
731
|
|
|
707
732
|
// 排行榜详情歌曲。匿名可用;返回带 songmid 的歌曲对象(可直接播放)。
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
const
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
733
|
+
// 用 musicu 的 music.musicToplist.Toplist/GetDetail 分页取数:offset/num 支持翻页,
|
|
734
|
+
// 可加载更多(旧版 fcg_v8_toplist_cp.fcg 无法翻页且最多只返回 50 首)。
|
|
735
|
+
// 返回 { id, name, cover, updateTime, total, hasMore, songs }。
|
|
736
|
+
export async function getTopListSongs(topId, cookie = '', offset = 0, num = 30) {
|
|
737
|
+
const off = Math.max(0, parseInt(offset, 10) || 0)
|
|
738
|
+
const size = Math.max(1, Math.min(50, parseInt(num, 10) || 30))
|
|
739
|
+
const body = {
|
|
740
|
+
comm: { ct: 24, cv: 0, format: 'json', platform: 'yqq.json', needNewCode: 1 },
|
|
741
|
+
req_0: { module: 'music.musicToplist.Toplist', method: 'GetDetail', param: { topId: Number(topId), offset: off, num: size, withTags: true } },
|
|
742
|
+
}
|
|
743
|
+
const j = await postMusicu(body, cookie)
|
|
744
|
+
const res = j.req_0 || j['music.musicToplist.Toplist']
|
|
745
|
+
if (!res || Number(res.code) !== 0) throw new Error('获取榜单失败:' + ((res && (res.message || res.msg)) || ('code ' + (res && res.code))))
|
|
746
|
+
const data = (res.data && res.data.data) || {}
|
|
747
|
+
const list = (res.data && Array.isArray(res.data.songInfoList) ? res.data.songInfoList : []).map((x) => {
|
|
748
|
+
const s = (x && x.songInfo) || x || {}
|
|
719
749
|
return {
|
|
720
|
-
id: s.
|
|
721
|
-
songid: s.songid, songtype: s.songtype || 0,
|
|
750
|
+
id: s.mid, songmid: s.mid, title: dec(s.name || s.title || ''),
|
|
751
|
+
songid: s.id || s.songid, songtype: s.type || s.songtype || 0,
|
|
722
752
|
artists: (s.singer || []).map((v) => dec(v.name)),
|
|
723
|
-
album: dec(s.
|
|
724
|
-
payplay: s.pay
|
|
725
|
-
rank: x && typeof x === 'object' && 'rank' in x ? x.rank : 0,
|
|
753
|
+
album: s.album ? dec(s.album.name || '') : '', interval: s.interval,
|
|
754
|
+
payplay: (s.pay && (s.pay.payplay || s.pay.pay_play)) || 0, source: 'qq',
|
|
726
755
|
}
|
|
727
756
|
}).filter((s) => s.songmid)
|
|
757
|
+
const total = Number(data.totalNum) || list.length
|
|
728
758
|
return {
|
|
729
759
|
id: String(topId),
|
|
730
|
-
name: dec(
|
|
731
|
-
|
|
732
|
-
|
|
760
|
+
name: dec(data.title || ''),
|
|
761
|
+
cover: httpsCover((data.headPicUrl || data.frontPicUrl || '').replace(/R[0-9]+x[0-9]+/, 'R300x300')),
|
|
762
|
+
intro: dec(data.intro || ''),
|
|
763
|
+
updateTime: data.updateTime || data.period || '',
|
|
764
|
+
total,
|
|
765
|
+
hasMore: off + list.length < total,
|
|
733
766
|
songs: list,
|
|
734
767
|
}
|
|
735
768
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-music-player",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "DeepSeek Harness 本地音乐 + AI 讲书插件:Host 扫描音乐目录并以 HTTP 流式提供音频、解析 .txt 小说结构并经 MiMo TTS 合成朗读,浏览器侧提供播放条/播放面板/章节目录跳转/多声音选择/实时频谱,并注册 music_play 模型工具",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|