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 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)) }, '⏮') : null,
1878
- hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: '播放/暂停', onClick: togglePlay }, s.playing ? '⏸' : '▶') : null,
1879
- hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: isBook ? '下一章' : '下一首', onClick: () => (isBook ? stepBook(1) : step(1)) }, '⏭') : null,
1880
- hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: '停止', onClick: stop }, '⏹') : null,
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
- const d = await json('/dsh-music/qq/top-songs?topId=' + encodeURIComponent(top.id));
2248
- if (d && d.ok) setTopDetail({ ...d.toplist, id: top.id, name: top.name, cover: top.cover || '' }); else setBrowseErr((d && d.error) || '加载榜单失败');
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 || '歌单' }); else setBrowseErr((d && d.error) || '加载失败');
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
- React.createElement('button', {
2391
- className: 'dsh-music-playlist-mini add',
2392
- title: '加入我的歌单',
2393
- onClick: (e) => { e.stopPropagation(); openQqJoin(song, e); },
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
- return React.createElement('button', {
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' }, topDetail.updateTime ? '更新于 ' + topDetail.updateTime : ''))),
2661
- rows);
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
- '.dsh-music-track.active { color: var(--dsh-music-accent, #2f9e6e); }\n' +
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 .dsh-music-track { color: var(--dsh-music-accent, #2f9e6e); }\n' +
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 .dsh-music-track { color: var(--dsh-music-accent, #2f9e6e); }\n' +
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
- export async function getTopListSongs(topId, cookie = '') {
709
- const p = new URLSearchParams()
710
- p.set('topid', String(topId)); p.set('page', 'detail'); p.set('type', 'top'); p.set('tpl', '3')
711
- p.set('song_num', '30'); p.set('format', 'json')
712
- const apiURL = `https://c.y.qq.com/v8/fcg-bin/fcg_v8_toplist_cp.fcg?${p.toString()}`
713
- const res = await fetchWithTimeout(apiURL, { headers: { 'User-Agent': UA, 'Referer': 'https://y.qq.com/', ...(cookie ? { Cookie: cookie } : {}) } })
714
- let text = await res.text()
715
- if (text.trimStart().startsWith('MusicJsonCallback(')) { const i = text.indexOf('('); const e = text.lastIndexOf(')'); text = text.slice(i + 1, e) }
716
- const j = JSON.parse(text)
717
- const list = (j.songlist || []).map((x) => {
718
- const s = (x && x.data) || x || {}
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.songmid, songmid: s.songmid, title: dec(s.songname || s.name || ''),
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.albumname), interval: s.interval,
724
- payplay: s.pay ? s.pay.payplay : 0, source: 'qq',
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((j.topinfo && (j.topinfo.ListName || j.topinfo.title)) || j.topTitle || ''),
731
- intro: dec((j.topinfo && (j.topinfo.info || j.topinfo.intro)) || ''),
732
- updateTime: j.date || '', total: j.total_song_num || list.length,
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",
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",