dsh-music-player 0.1.3 → 0.1.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 +1 -0
- package/lib/client.js +52 -4
- package/lib/index.js +40 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ DeepSeek Harness 本地音乐库播放器插件(bundle)。
|
|
|
11
11
|
- 本地音频流式播放(HTTP Range),刷新后断点续播
|
|
12
12
|
- 顺序播放、单曲循环、乱序播放三种模式
|
|
13
13
|
- 实时 7 段频谱可视化(解码音频包络驱动)
|
|
14
|
+
- 播放时申请屏幕唤醒锁,防止听歌时熄屏/休眠(支持 Wake Lock 的浏览器,如 Chrome/Edge)
|
|
14
15
|
- 播放列表面板可自由拖动,位置跨刷新记忆
|
|
15
16
|
- `music_play` 模型工具:agent 可按关键词让浏览器播放
|
|
16
17
|
- 支持的格式:`mp3 / m4a / m4b / aac / flac / wav / ogg / opus / webm / aiff`(自动递归扫描子目录,上限 500 首)
|
package/lib/client.js
CHANGED
|
@@ -379,13 +379,40 @@ window.__ModuleLoader__.load({
|
|
|
379
379
|
audio.load();
|
|
380
380
|
set({ currentId: null, currentName: null, playing: false, position: 0, duration: 0, pendingId: null, pendingName: null, vizState: 'ok' });
|
|
381
381
|
clearPref(PREF_PLAYBACK);
|
|
382
|
+
releaseWakeLock();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ---- Screen Wake Lock: keep the screen awake while music is playing ----
|
|
386
|
+
// While audio plays we request a screen wake lock so the display (and, for
|
|
387
|
+
// most power policies, the system) doesn't blank or sleep mid-song. Tabs
|
|
388
|
+
// can't stop OS-level deep sleep, but holding a wake lock while visible
|
|
389
|
+
// covers the common "screen blanked while I listened" case. Unsupported
|
|
390
|
+
// browsers (e.g. Safari) silently skip it — never fatal.
|
|
391
|
+
let wakeLock = null;
|
|
392
|
+
const wakeLockSupported = (typeof navigator !== 'undefined') && ('wakeLock' in navigator);
|
|
393
|
+
async function acquireWakeLock() {
|
|
394
|
+
if (!wakeLockSupported || !store.playing) return;
|
|
395
|
+
if (wakeLock !== null) return; // already held
|
|
396
|
+
try {
|
|
397
|
+
const sentinel = await navigator.wakeLock.request('screen');
|
|
398
|
+
sentinel.addEventListener('release', () => { if (wakeLock === sentinel) wakeLock = null; });
|
|
399
|
+
wakeLock = sentinel;
|
|
400
|
+
} catch (e) {
|
|
401
|
+
wakeLock = null; // denied or transient — non-fatal
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function releaseWakeLock() {
|
|
405
|
+
if (wakeLock !== null) {
|
|
406
|
+
try { wakeLock.release(); } catch (e) {}
|
|
407
|
+
wakeLock = null;
|
|
408
|
+
}
|
|
382
409
|
}
|
|
383
410
|
|
|
384
411
|
function bindAudio() {
|
|
385
412
|
const onTime = () => set({ position: audio.currentTime || 0 });
|
|
386
413
|
const onDur = () => set({ duration: audio.duration || 0 });
|
|
387
|
-
const onPlay = () => set({ playing: true, error: null });
|
|
388
|
-
const onPause = () => { set({ playing: false }); savePlayback(); };
|
|
414
|
+
const onPlay = () => { set({ playing: true, error: null }); acquireWakeLock(); };
|
|
415
|
+
const onPause = () => { set({ playing: false }); savePlayback(); releaseWakeLock(); };
|
|
389
416
|
const onEnded = () => {
|
|
390
417
|
if (store.mode === 'single' && store.currentId !== null) {
|
|
391
418
|
audio.currentTime = 0;
|
|
@@ -832,14 +859,35 @@ window.__ModuleLoader__.load({
|
|
|
832
859
|
ctx.effect(() => {
|
|
833
860
|
const unbind = bindAudio();
|
|
834
861
|
startRaf();
|
|
835
|
-
|
|
862
|
+
// Browsers auto-release a wake lock when the page is hidden; re-acquire
|
|
863
|
+
// on return if playback is still running, and drop it on hide so we
|
|
864
|
+
// don't hold it while the tab is backgrounded.
|
|
865
|
+
const onVis = () => {
|
|
866
|
+
if (document.hidden) releaseWakeLock();
|
|
867
|
+
else acquireWakeLock();
|
|
868
|
+
};
|
|
869
|
+
document.addEventListener('visibilitychange', onVis);
|
|
870
|
+
return () => { document.removeEventListener('visibilitychange', onVis); stopRaf(); unbind(); closeDecodeCtx(); releaseWakeLock(); };
|
|
836
871
|
}, 'music-player: audio + viz engine');
|
|
837
872
|
|
|
838
873
|
loadTracks();
|
|
839
874
|
|
|
840
875
|
const intentTimer = setInterval(() => {
|
|
841
876
|
jsonGet('/dsh-music/intent').then((intent) => {
|
|
842
|
-
if (intent === null || typeof intent !== 'object'
|
|
877
|
+
if (intent === null || typeof intent !== 'object') return;
|
|
878
|
+
const action = intent.action || 'play';
|
|
879
|
+
// Transport commands operate on the current playback state (no track id).
|
|
880
|
+
if (action === 'pause') { audio.pause(); set({ playing: false }); return; }
|
|
881
|
+
if (action === 'resume') {
|
|
882
|
+
const p = audio.play();
|
|
883
|
+
if (p !== undefined && typeof p.catch === 'function') p.catch(() => set({ error: '\u64ad\u653e\u5931\u8d25' }));
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (action === 'stop') { stop(); return; }
|
|
887
|
+
if (action === 'next') { step(1); return; }
|
|
888
|
+
if (action === 'prev') { step(-1); return; }
|
|
889
|
+
// play (default): needs a track id.
|
|
890
|
+
if (intent.id === undefined) return;
|
|
843
891
|
set({ pendingId: intent.id, pendingName: intent.name || '' });
|
|
844
892
|
const track = trackById(intent.id);
|
|
845
893
|
if (track !== null) {
|
package/lib/index.js
CHANGED
|
@@ -353,18 +353,26 @@ export function apply(ctx) {
|
|
|
353
353
|
ctx.effect(() => ctx.webServer.register({ kind: 'prefix', path: '/dsh-music', handler: serve }), 'music-player: routes')
|
|
354
354
|
|
|
355
355
|
// ---- model tool: music_play ----
|
|
356
|
+
const PLAY_ACTIONS = ['play', 'pause', 'resume', 'stop', 'next', 'prev']
|
|
356
357
|
const tool = {
|
|
357
358
|
name: 'music_play',
|
|
358
|
-
description: '
|
|
359
|
+
description: '控制 DSH 本地音乐库的播放。播放时可按歌曲名/歌手关键词搜索并播放(不传 query 则播放第一首);也可用 action 执行暂停/继续/停止/下一首/上一首。',
|
|
359
360
|
parameters: {
|
|
360
361
|
type: 'object',
|
|
361
362
|
additionalProperties: false,
|
|
362
|
-
properties: {
|
|
363
|
+
properties: {
|
|
364
|
+
query: { type: 'string', description: '歌曲名/歌手关键词,用于搜索并播放。仅当 action 为 play(默认)时使用,可留空' },
|
|
365
|
+
action: { type: 'string', enum: PLAY_ACTIONS, description: '要执行的动作:play 播放(默认)、pause 暂停、resume 继续、stop 停止、next 下一首、prev 上一首' },
|
|
366
|
+
},
|
|
363
367
|
},
|
|
364
368
|
output: {
|
|
365
369
|
schema: {
|
|
366
370
|
type: 'object', additionalProperties: false,
|
|
367
|
-
properties: {
|
|
371
|
+
properties: {
|
|
372
|
+
action: { type: 'string' }, played: { type: 'boolean' },
|
|
373
|
+
track: { type: 'string' }, matches: { type: 'number' }, count: { type: 'number' },
|
|
374
|
+
notice: { type: 'string' },
|
|
375
|
+
},
|
|
368
376
|
},
|
|
369
377
|
render(_args, value) {
|
|
370
378
|
return [{ type: 'text', text: (value && value.notice) || (value && value.track ? '已请求播放:' + value.track : '音乐库为空') }]
|
|
@@ -372,22 +380,39 @@ export function apply(ctx) {
|
|
|
372
380
|
},
|
|
373
381
|
async execute(args) {
|
|
374
382
|
await ensureStarted()
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
383
|
+
const count = tracks.length
|
|
384
|
+
const action = args && typeof args.action === 'string' && PLAY_ACTIONS.includes(args.action) ? args.action : 'play'
|
|
385
|
+
|
|
386
|
+
if (count === 0) {
|
|
387
|
+
const notice = '本地音乐库为空。请打开播放列表面板,点击「选择音乐目录」配置音乐目录。'
|
|
388
|
+
return { action, played: false, track: '', matches: 0, count: 0, notice }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Non-play actions just relay a transport command to the browser player.
|
|
392
|
+
if (action !== 'play') {
|
|
393
|
+
pendingIntent = { action }
|
|
394
|
+
const labels = {
|
|
395
|
+
pause: '已请求暂停播放', resume: '已请求继续播放', stop: '已请求停止播放',
|
|
396
|
+
next: '已请求播放下一首', prev: '已请求播放上一首',
|
|
379
397
|
}
|
|
398
|
+
const notice = labels[action] + '。若浏览器拦截自动操作,请在播放条上点击对应按钮。'
|
|
399
|
+
return { action, played: false, track: '', matches: 0, count, notice }
|
|
380
400
|
}
|
|
401
|
+
|
|
402
|
+
// play: search (exact match first, then substring) and pick the first hit.
|
|
381
403
|
const query = args && typeof args.query === 'string' ? args.query.trim().toLowerCase() : ''
|
|
382
|
-
const
|
|
383
|
-
if (
|
|
384
|
-
|
|
404
|
+
const pool = query === '' ? tracks : tracks.filter((t) => t.name.toLowerCase().includes(query))
|
|
405
|
+
if (pool.length === 0) {
|
|
406
|
+
const notice = '没有找到包含「' + (args && args.query) + '」的音乐(音乐库共 ' + count + ' 首)。'
|
|
407
|
+
return { action, played: false, track: '', matches: 0, count, notice }
|
|
385
408
|
}
|
|
386
|
-
|
|
387
|
-
|
|
409
|
+
// Prefer an exact (case-insensitive) filename match over the first substring hit.
|
|
410
|
+
const pick = query === '' ? pool[0]
|
|
411
|
+
: (tracks.find((t) => t.name.toLowerCase() === query) || pool[0])
|
|
412
|
+
pendingIntent = { action: 'play', id: pick.id, name: pick.name }
|
|
388
413
|
return {
|
|
389
|
-
played: true, track: pick.name,
|
|
390
|
-
notice: '已请求播放「' + pick.name + '
|
|
414
|
+
action, played: true, track: pick.name, matches: pool.length, count,
|
|
415
|
+
notice: '已请求播放「' + pick.name + '」(匹配 ' + pool.length + ' / 共 ' + count + ' 首)。浏览器可能拦截自动播放,请在页面播放条上点击一次▶解锁。',
|
|
391
416
|
}
|
|
392
417
|
},
|
|
393
418
|
}
|
|
@@ -396,7 +421,7 @@ export function apply(ctx) {
|
|
|
396
421
|
// ---- light prompt hint so the agent knows it can play local music ----
|
|
397
422
|
ctx.systemPrompt.section({
|
|
398
423
|
name: 'tool:music-player', order: 116,
|
|
399
|
-
text: '本机已挂载本地音乐播放器:可用 music_play 工具按关键词播放 ~/Music
|
|
424
|
+
text: '本机已挂载本地音乐播放器:可用 music_play 工具按关键词播放 ~/Music(或设置的目录)里的音乐,并支持 action 暂停/继续/停止/上下首控制。',
|
|
400
425
|
})
|
|
401
426
|
|
|
402
427
|
void ensureStarted()
|