dsh-music-player 0.5.0 → 0.5.2

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
@@ -23,9 +23,11 @@ DeepSeek Harness 本地音乐/小说播放插件。
23
23
 
24
24
  ## 截图
25
25
 
26
- ![播放条](assets/screenshot-bar.png)
26
+ ![播放本地音乐](assets/screenshot-bar.png)
27
27
 
28
- ![实时频谱](assets/screenshot-spectrum.png)
28
+ ![播放QQ音乐](assets/screenshot-qq.png)
29
+
30
+ ![播放AI讲书](assets/screenshot-spectrum.png)
29
31
 
30
32
  ![播放面板](assets/screenshot-panel.png)
31
33
 
@@ -117,17 +119,17 @@ dsh plugin --profile <profile> add ./dsh-music-player-0.1.0.tgz
117
119
  - 新歌:新歌速递(最新/内地/港台/欧美/韩国等)。
118
120
  - 搜索:搜歌曲与歌单,带搜索历史(localStorage,最近 10 条)。
119
121
  - **播放**:点击任意歌曲即可播放(同一播放条 + 频谱);VIP 标识显示在歌名后,行尾显示歌手名。
120
- - **收藏**:播放条爱心按钮把当前在线曲目收藏到 QQ 音乐「我喜欢」(dirId 201);已收藏歌曲爱心实时点亮。
122
+ - **收藏**:播放条爱心按钮把当前在线曲目收藏到 QQ 音乐「我喜欢」,已收藏歌曲爱心实时点亮。
121
123
  - **续播**:在线播放进度(当前曲目 + 队列)刷新后自动恢复,点 ▶ 续播。
122
124
  - 在线曲目不占本地曲库的 500 首上限,与本地/讲书完全隔离。
123
125
 
124
126
  ## AI 讲书
125
127
 
126
- 把本地 `.txt` 小说交给 AI 朗读(复用 DSH 已配置的 xiaomi/MiMo TTS)。**AI 语音目前仅支持 xiaomi 提供方(限时免费),请在设置中配置好再使用此功能。**
128
+ 把本地 `.txt` 小说交给 AI 朗读。**AI 语音目前仅支持xiaomi提供方(限时免费),请在设置中配置好再使用此功能。**
127
129
 
128
130
  ### 前置
129
131
 
130
- 在 DSH 的模型设置里配置一个 xiaomi/MiMo TTS provider(含 api key)。未配置时,小说列表会提示"未配置 xiaomi/MiMo TTS 模型"。
132
+ 在 DSH 的模型设置里配置一个xiaomi提供方(含 api key)。未配置时,小说列表会提示"未配置xiaomi提供方"。
131
133
 
132
134
  ### 使用
133
135
 
package/lib/client.js CHANGED
@@ -710,6 +710,10 @@ window.__ModuleLoader__.load({
710
710
  let shuffleQueue = [];
711
711
  let shufflePos = -1;
712
712
  let shuffleScopeKey = null;
713
+ // 在线 QQ 队列连续失败的跳过次数:某首歌因版权下架/拿不到地址而触发
714
+ // <audio> error 时自动跳到下一首;连续跳过次数达到队列长度(整列都试过)
715
+ // 即停止报错——且停止后不再 step,杜绝无限循环跳歌。成功播放(onPlay)清零。
716
+ let qqErrorSkipCount = 0;
713
717
  function buildShuffleQueue(anchorId) {
714
718
  const ids = activeIds();
715
719
  // Fisher-Yates
@@ -778,7 +782,7 @@ window.__ModuleLoader__.load({
778
782
  bookRestorePos = -1;
779
783
  audio.src = track.url;
780
784
  audio.load();
781
- set({ currentId: id, currentName: track.name, currentArtists: track.artists || store.currentArtists || [], pendingId: null, pendingName: null, error: null, tocOpen: false, currentSection: '', qqFaved: false });
785
+ set({ currentId: id, currentName: track.name, currentArtists: track.artists || [], pendingId: null, pendingName: null, error: null, tocOpen: false, currentSection: '', qqFaved: false });
782
786
  syncShufflePos();
783
787
  loadEnvelope(id, track.url);
784
788
  prefetchNext();
@@ -947,7 +951,7 @@ window.__ModuleLoader__.load({
947
951
  // near-instant (server-side synthesis cache) so we don't flash a spinner
948
952
  // at every chunk boundary. Only user-initiated plays show it.
949
953
  const showBuffer = !silent;
950
- set({ currentId: 'book:' + id, currentName: book.name, pendingId: null, pendingName: null, error: null, vizState: 'ok' });
954
+ set({ currentId: 'book:' + id, currentName: book.name, currentArtists: [], pendingId: null, pendingName: null, error: null, vizState: 'ok' });
951
955
  // Load the chapter structure for the toc + the current-section label (the
952
956
  // current section is derived from the chunk index once the meta arrives).
953
957
  const startFrom = from;
@@ -1310,7 +1314,7 @@ window.__ModuleLoader__.load({
1310
1314
  set({ duration: (bookTimeBase() + audio.duration) });
1311
1315
  }
1312
1316
  };
1313
- const onPlay = () => { set({ playing: true, error: null }); acquireWakeLock(); };
1317
+ const onPlay = () => { qqErrorSkipCount = 0; set({ playing: true, error: null }); acquireWakeLock(); };
1314
1318
  const onPause = () => { set({ playing: false }); savePlayback(); releaseWakeLock(); };
1315
1319
  const onEnded = () => {
1316
1320
  // A novel plays chunk-by-chunk: when a chunk ends, auto-advance to the
@@ -1359,6 +1363,17 @@ window.__ModuleLoader__.load({
1359
1363
  }).catch(() => {});
1360
1364
  }
1361
1365
  } else {
1366
+ // 在线 QQ 队列:某首歌因版权下架/拿不到播放地址而加载失败时,自动
1367
+ // 跳到下一首继续播放(不因单曲失败中断整个队列);只有队列里就这一
1368
+ // 首、或连续跳过次数已达队列长度(整列都试过)才停下报错——且停止后
1369
+ // 不再 step,杜绝无限循环跳歌。
1370
+ const isQQTrack = store.currentId !== null && String(store.currentId).startsWith('qq:');
1371
+ const qLen = (store.qqQueue || []).length;
1372
+ if (isQQTrack && qLen > 1 && qqErrorSkipCount < qLen) {
1373
+ qqErrorSkipCount++;
1374
+ step(1);
1375
+ return;
1376
+ }
1362
1377
  set({ error: '音频加载或解码失败', playing: false });
1363
1378
  }
1364
1379
  };
@@ -1432,6 +1447,7 @@ window.__ModuleLoader__.load({
1432
1447
  // (togglePlay), and the readout is pinned to the restored values.
1433
1448
  set({
1434
1449
  currentId: track.id, currentName: track.name,
1450
+ currentArtists: track.artists || [],
1435
1451
  position: pos, duration: savedDur,
1436
1452
  pendingId: null, pendingName: null, error: null, scope,
1437
1453
  });
@@ -1475,7 +1491,7 @@ window.__ModuleLoader__.load({
1475
1491
  // <audio> element here (avoiding the Chromium 'getTopURL' quirk on
1476
1492
  // refresh); togglePlay loads + seeks the chunk when the user resumes.
1477
1493
  set({
1478
- currentId: 'book:' + book.id, currentName: book.name,
1494
+ currentId: 'book:' + book.id, currentName: book.name, currentArtists: [],
1479
1495
  position: base + pos, duration: base + (Number.isFinite(audio.duration) ? audio.duration : 0),
1480
1496
  pendingId: null, pendingName: null, error: null, playing: false,
1481
1497
  bookBuffering: false, bookBufferingSilent: false, bookError: '', bookBufferingSince: 0,
@@ -1563,7 +1579,8 @@ window.__ModuleLoader__.load({
1563
1579
  tracks: result.tracks || [], books: result.books || [],
1564
1580
  count: result.count || 0, loading: false, error: null,
1565
1581
  });
1566
- restoreLatest(result.tracks || []);
1582
+ // 换目录只刷新列表,不调用 restoreLatest:否则播放中的在线 QQ 曲目会
1583
+ // 触发 restorePlayback 把 tab 强制切回「QQ音乐」(选完目录被跳回的问题)。
1567
1584
  } else {
1568
1585
  set({ loading: false, error: (result && result.error) || '设置目录失败' });
1569
1586
  }
@@ -1975,6 +1992,9 @@ window.__ModuleLoader__.load({
1975
1992
  }
1976
1993
  }
1977
1994
 
1995
+ // 在线 QQ 面板的「上次所在层」只在本次会话首次挂载时恢复一次,避免切 tab
1996
+ // (QQ 音乐 ↔ 本地音乐/AI讲书)重新挂载时又被 localStorage 里的旧层拉回播放列表页。
1997
+ let qqUiRestored = false;
1978
1998
  function QQOnlinePanel() {
1979
1999
  const s = useStore();
1980
2000
  const [loggedIn, setLoggedIn] = useState(s.qqLoggedIn || false);
@@ -2066,7 +2086,8 @@ window.__ModuleLoader__.load({
2066
2086
  // 默认落在「我的歌单」tab:登录态下加载我的歌单;未登录时主 UI 不显示。
2067
2087
  loadMine();
2068
2088
  loadRecommended();
2069
- restoreUi(loadUi());
2089
+ // 仅本次会话首次挂载时恢复上次所在层;之后切 tab 重挂不再拉回旧层。
2090
+ if (!qqUiRestored) { qqUiRestored = true; restoreUi(loadUi()); }
2070
2091
  // 点击搜索框外时关闭历史下拉。
2071
2092
  const onDocClick = (e) => { if (histRef.current !== null && !histRef.current.contains(e.target)) setHistOpen(false); };
2072
2093
  document.addEventListener('mousedown', onDocClick);
@@ -3034,7 +3055,7 @@ window.__ModuleLoader__.load({
3034
3055
  if (track !== null) {
3035
3056
  audio.src = track.url;
3036
3057
  audio.load();
3037
- set({ currentId: intent.id, currentName: track.name, error: null, scope: { kind: 'library' } });
3058
+ set({ currentId: intent.id, currentName: track.name, currentArtists: track.artists || [], error: null, scope: { kind: 'library' } });
3038
3059
  loadEnvelope(intent.id, track.url);
3039
3060
  prefetchNext();
3040
3061
  savePlayback();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-music-player",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "DeepSeek Harness 本地音乐 + AI 讲书插件:Host 扫描音乐目录并以 HTTP 流式提供音频、解析 .txt 小说结构并经 MiMo TTS 合成朗读,浏览器侧提供播放条/播放面板/章节目录跳转/多声音选择/实时频谱,并注册 music_play 模型工具",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",