dsh-music-player 0.1.0 → 0.1.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 +48 -5
- package/lib/client.js +66 -2
- package/lib/index.js +4 -0
- package/package.json +12 -1
package/README.md
CHANGED
|
@@ -9,24 +9,40 @@ DeepSeek Harness 本地音乐库播放器插件(bundle)。
|
|
|
9
9
|
- 本地音频流式播放,支持 seek / 断点续播
|
|
10
10
|
- 顺序播放、单曲循环、乱序播放三种模式
|
|
11
11
|
- 实时 7 段频谱可视化(解码音频包络驱动)
|
|
12
|
+
- 播放列表面板可自由拖动,位置跨刷新记忆
|
|
12
13
|
- `music_play` 模型工具:agent 可按关键词让浏览器播放
|
|
13
14
|
- 支持的格式:`mp3 / m4a / m4b / aac / flac / wav / ogg / opus / webm / aiff`(自动递归扫描子目录,上限 500 首)
|
|
14
15
|
|
|
16
|
+
## 截图
|
|
17
|
+
|
|
18
|
+

|
|
19
|
+
|
|
20
|
+

|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
15
24
|
## 安装
|
|
16
25
|
|
|
17
26
|
需要已安装 `dsh` CLI。
|
|
18
27
|
|
|
19
|
-
|
|
28
|
+
### 从 npm 安装(推荐,已发布到 registry)
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
# 把 <profile> 换成实际 profile 名,如 web
|
|
32
|
+
dsh plugin --profile <profile> add dsh-music-player
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 从 GitHub 安装(备用来源)
|
|
20
36
|
|
|
21
37
|
```sh
|
|
22
|
-
#
|
|
38
|
+
# 把 <profile> 换成实际 profile 名,如 web
|
|
23
39
|
dsh plugin --profile <profile> add github:kendu76/dsh-music-player
|
|
24
40
|
```
|
|
25
41
|
|
|
26
|
-
> 项目是手写的纯 JS(`lib/` 直接是发布产物),**没有**需要从源码构建的步骤,因此从 GitHub 直装即可使用,无需像 TypeScript 包那样为构建脚本授权。
|
|
42
|
+
> 项目是手写的纯 JS(`lib/` 直接是发布产物),**没有**需要从源码构建的步骤,因此从 GitHub/npm 直装即可使用,无需像 TypeScript 包那样为构建脚本授权。
|
|
27
43
|
|
|
28
44
|
安装后重启 DSH,打开 Web GUI:
|
|
29
|
-
-
|
|
45
|
+
- 聊天输入区上方会出现「本地音乐播放器」播放条
|
|
30
46
|
- 点击右侧「列表」按钮打开播放面板
|
|
31
47
|
- 在面板顶部点击「选择音乐目录」并选定音乐目录(默认 `~/Music`),自动递归扫描
|
|
32
48
|
- 之后可直接在对话框里让 agent 播放,例如「播放周杰伦的歌」
|
|
@@ -61,11 +77,38 @@ dsh plugin --profile <profile> add ./dsh-music-player-0.1.0.tgz
|
|
|
61
77
|
|
|
62
78
|
## 开发
|
|
63
79
|
|
|
80
|
+
需要 Node.js ≥ 20(vitest 建议 20.19+)与 npm。开发依赖仅 `vitest`:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
npm install
|
|
84
|
+
npm test # 跑 vitest 测试套件
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
修改 `lib/` 后,在本机 profile 里用 link 方式本地调试并验证:
|
|
88
|
+
|
|
64
89
|
```sh
|
|
65
|
-
# 修改 lib/ 后,在本机 profile 里用 link 方式本地调试
|
|
66
90
|
dsh plugin --profile <profile> add ./ # 或直接改 profile 里的 link 目标
|
|
67
91
|
```
|
|
68
92
|
|
|
93
|
+
项目结构、测试策略与发布流程详见 [CONTRIBUTING.md](CONTRIBUTING.md)。
|
|
94
|
+
|
|
95
|
+
## 常见问题
|
|
96
|
+
|
|
97
|
+
**播放没有声音 / 显示"浏览器拦截了自动播放"?**
|
|
98
|
+
浏览器安全策略禁止未经交互的音频播放。首次自动播放被拦截是正常的——在播放条上点一次 ▶ 即可解锁,之后恢复播放。
|
|
99
|
+
|
|
100
|
+
**音乐面板显示"暂无音乐"或"不是有效的音乐目录"?**
|
|
101
|
+
点面板顶部「选择音乐目录」,选一个包含音频文件的实际目录(默认 `~/Music`)。目录路径不可读或不存在时会回退到默认目录而不是报错。
|
|
102
|
+
|
|
103
|
+
**改了音乐目录/新增了歌曲,但列表没更新?**
|
|
104
|
+
播放器在启动时扫描,并在每次“选择音乐目录”时重扫。想强制刷新当前目录,重新打开一次面板或点一次目录设置即可(扫描上限 500 首、递归子目录深度上限 4 层)。
|
|
105
|
+
|
|
106
|
+
**`music_play` 工具说"音乐库为空"?**
|
|
107
|
+
说明还没有可用的音乐目录。请先打开播放面板,点「选择音乐目录」配置一次。
|
|
108
|
+
|
|
109
|
+
**想支持更多音频格式?**
|
|
110
|
+
格式支持由 Host 端 `AUDIO_TYPES` 表驱动,在 `lib/index.js` 里加扩展名与 MIME 即可(播放器本身用浏览器原生 `<audio>` 解码,最终能否播放还取决于浏览器对该编码的支持)。
|
|
111
|
+
|
|
69
112
|
## License
|
|
70
113
|
|
|
71
114
|
[MIT](LICENSE) © kendu76
|
package/lib/client.js
CHANGED
|
@@ -28,9 +28,21 @@ window.__ModuleLoader__.load({
|
|
|
28
28
|
const PREF_VOL = 'dsh-music-volume';
|
|
29
29
|
const PREF_PLAYBACK = 'dsh-music-playback';
|
|
30
30
|
const PREF_ROOT = 'dsh-music-root';
|
|
31
|
+
const PREF_PANEL_POS = 'dsh-music-panel-pos';
|
|
31
32
|
const loadPref = (k) => { try { return localStorage.getItem(k); } catch (e) { return null; } };
|
|
32
33
|
const savePref = (k, v) => { try { localStorage.setItem(k, v); } catch (e) {} };
|
|
33
34
|
const clearPref = (k) => { try { localStorage.removeItem(k); } catch (e) {} };
|
|
35
|
+
// Restore the playback-panel position ({x,y,h}) previously saved by dragging, if any.
|
|
36
|
+
function loadPanelPos() {
|
|
37
|
+
const raw = loadPref(PREF_PANEL_POS);
|
|
38
|
+
if (raw === null) return null;
|
|
39
|
+
try {
|
|
40
|
+
const p = JSON.parse(raw);
|
|
41
|
+
if (p && typeof p.x === 'number' && typeof p.y === 'number'
|
|
42
|
+
&& typeof p.h === 'number' && p.h > 0) return p;
|
|
43
|
+
} catch (e) {}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
34
46
|
const jsonGet = (url) => fetch(url, { cache: 'no-store' }).then((r) => r.json());
|
|
35
47
|
|
|
36
48
|
// ---- engine + shared store (React re-renders on set) ----
|
|
@@ -627,6 +639,52 @@ window.__ModuleLoader__.load({
|
|
|
627
639
|
const s = useStore();
|
|
628
640
|
const listRef = useRef(null);
|
|
629
641
|
const panelRef = useRef(null);
|
|
642
|
+
// Draggable panel position ({x, y, h} left/top/height once dragged; null = default right/bottom)
|
|
643
|
+
const [pos, setPos] = useState(loadPanelPos);
|
|
644
|
+
const dragRef = useRef(null);
|
|
645
|
+
|
|
646
|
+
// Once the panel is dragged we switch from CSS right/bottom anchoring to an
|
|
647
|
+
// explicit left/top/height. Locking the height matters: with only top+left set
|
|
648
|
+
// and no height, a fixed element whose CSS also sets bottom collapses to fit
|
|
649
|
+
// the leftover space, so the panel's height jumps while dragging.
|
|
650
|
+
const style = pos === null ? null : { left: pos.x, top: pos.y, height: pos.h };
|
|
651
|
+
|
|
652
|
+
const onHeadDown = (e) => {
|
|
653
|
+
if (e.button !== undefined && e.button !== 0) return;
|
|
654
|
+
// don't start a drag from the close button
|
|
655
|
+
if (e.target.closest && e.target.closest('.dsh-music-icon-btn')) return;
|
|
656
|
+
const el = panelRef.current;
|
|
657
|
+
if (el === null) return;
|
|
658
|
+
const rect = el.getBoundingClientRect();
|
|
659
|
+
const h = pos !== null ? pos.h : rect.height;
|
|
660
|
+
const next = { x: pos !== null ? pos.x : rect.left, y: pos !== null ? pos.y : rect.top, h };
|
|
661
|
+
dragRef.current = {
|
|
662
|
+
startX: e.clientX, startY: e.clientY,
|
|
663
|
+
originX: next.x, originY: next.y, h,
|
|
664
|
+
};
|
|
665
|
+
setPos(next);
|
|
666
|
+
savePref(PREF_PANEL_POS, JSON.stringify(next));
|
|
667
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
668
|
+
};
|
|
669
|
+
const onHeadMove = (e) => {
|
|
670
|
+
const d = dragRef.current;
|
|
671
|
+
if (d === null) return;
|
|
672
|
+
let x = d.originX + (e.clientX - d.startX);
|
|
673
|
+
let y = d.originY + (e.clientY - d.startY);
|
|
674
|
+
const el = panelRef.current;
|
|
675
|
+
if (el !== null) {
|
|
676
|
+
x = Math.max(0, Math.min(x, window.innerWidth - el.offsetWidth));
|
|
677
|
+
y = Math.max(0, Math.min(y, window.innerHeight - el.offsetHeight));
|
|
678
|
+
}
|
|
679
|
+
const next = { x, y, h: d.h };
|
|
680
|
+
setPos(next);
|
|
681
|
+
savePref(PREF_PANEL_POS, JSON.stringify(next));
|
|
682
|
+
};
|
|
683
|
+
const onHeadUp = (e) => {
|
|
684
|
+
dragRef.current = null;
|
|
685
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId);
|
|
686
|
+
};
|
|
687
|
+
|
|
630
688
|
useEffect(() => {
|
|
631
689
|
if (!s.panelOpen) return;
|
|
632
690
|
// Close the playlist panel when the user clicks outside it
|
|
@@ -658,8 +716,12 @@ window.__ModuleLoader__.load({
|
|
|
658
716
|
React.createElement('span', { className: 'dsh-music-track-size' }, t.size ? Math.round(t.size / 1024 / 1024 * 10) / 10 + ' MB' : ''),
|
|
659
717
|
);
|
|
660
718
|
});
|
|
661
|
-
return React.createElement('div', { className: 'dsh-music-panel', ref: panelRef },
|
|
662
|
-
React.createElement('div', {
|
|
719
|
+
return React.createElement('div', { className: 'dsh-music-panel', ref: panelRef, style },
|
|
720
|
+
React.createElement('div', {
|
|
721
|
+
className: 'dsh-music-panel-head dsh-music-panel-drag',
|
|
722
|
+
onPointerDown: onHeadDown, onPointerMove: onHeadMove, onPointerUp: onHeadUp,
|
|
723
|
+
},
|
|
724
|
+
React.createElement('span', { className: 'dsh-music-panel-grip', 'aria-hidden': true }, '\u283f'),
|
|
663
725
|
React.createElement('span', { className: 'dsh-music-panel-title' }, '\u64ad\u653e\u5217\u8868'),
|
|
664
726
|
React.createElement('button', { className: 'dsh-music-icon-btn', title: '\u5173\u95ed', onClick: () => set({ panelOpen: false }) }, '\u2715')),
|
|
665
727
|
React.createElement(DirectorySetting, null),
|
|
@@ -824,6 +886,8 @@ window.__ModuleLoader__.load({
|
|
|
824
886
|
'.dsh-music-bar .dsh-music-mode-menu { align-self: center; }\n' +
|
|
825
887
|
'.dsh-music-panel { position: fixed; right: 24px; bottom: 84px; width: 380px; max-height: 72vh; display: flex; flex-direction: column; gap: 8px; padding: 12px; background: var(--dsw-alias-bg-overlay, #1e1f22); border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.35)); border-radius: 12px; box-shadow: 0 12px 32px rgba(0,0,0,0.35); color: var(--dsw-alias-label-primary, #e6e6e6); font-size: 13px; z-index: 1000; pointer-events: auto; overflow: hidden; }\n' +
|
|
826
888
|
'.dsh-music-panel-head { display: flex; align-items: center; gap: 6px; }\n' +
|
|
889
|
+
'.dsh-music-panel-drag { cursor: move; touch-action: none; user-select: none; }\n' +
|
|
890
|
+
'.dsh-music-panel-grip { color: var(--dsw-alias-label-secondary, #8a8f98); font-size: 12px; letter-spacing: -1px; opacity: 0.7; }\n' +
|
|
827
891
|
'.dsh-music-panel-title { font-weight: 600; margin-right: auto; }\n' +
|
|
828
892
|
'.dsh-music-icon-btn { background: transparent; border: none; color: var(--dsw-alias-label-secondary, #8a8f98); cursor: pointer; font-size: 14px; padding: 2px 6px; border-radius: 6px; }\n' +
|
|
829
893
|
'.dsh-music-icon-btn:hover { color: var(--dsw-alias-label-primary, #e6e6e6); background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.06)); }\n' +
|
package/lib/index.js
CHANGED
|
@@ -232,6 +232,10 @@ export function apply(ctx) {
|
|
|
232
232
|
}
|
|
233
233
|
// audio streaming
|
|
234
234
|
if (req.method !== 'GET' && req.method !== 'HEAD') { res.writeHead(405); res.end(); return }
|
|
235
|
+
// Ensure the library scan completes before resolving a track, so a
|
|
236
|
+
// streaming/HEAD request that arrives before any manifest call (or at
|
|
237
|
+
// startup) still finds its track instead of spuriously 404ing.
|
|
238
|
+
await ensureStarted()
|
|
235
239
|
const id = pathname.slice('/dsh-music/'.length)
|
|
236
240
|
const track = tracks.find((t) => t.id === id)
|
|
237
241
|
if (track === undefined) { res.writeHead(404); res.end(); return }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-music-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "DeepSeek Harness 本地音乐库播放器:Host 扫描音乐目录并以 HTTP 流式提供音频,浏览器侧提供播放条/播放面板/设置页与实时频谱,并注册 music_play 模型工具",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -28,6 +28,14 @@
|
|
|
28
28
|
"url": "git+https://github.com/kendu76/dsh-music-player.git"
|
|
29
29
|
},
|
|
30
30
|
"license": "MIT",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:watch": "vitest",
|
|
37
|
+
"ci": "vitest run"
|
|
38
|
+
},
|
|
31
39
|
"dsh": {
|
|
32
40
|
"bundle": {
|
|
33
41
|
"patch": "./cordis.patch.yml"
|
|
@@ -39,5 +47,8 @@
|
|
|
39
47
|
},
|
|
40
48
|
"peerDependencies": {
|
|
41
49
|
"react": "^18.2.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"vitest": "^4.1.10"
|
|
42
53
|
}
|
|
43
54
|
}
|