dsh-music-player 0.2.0 → 0.2.1
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/lib/client.js +137 -48
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -52,13 +52,54 @@ window.__ModuleLoader__.load({
|
|
|
52
52
|
const PREF_MODE = 'dsh-music-mode';
|
|
53
53
|
const PREF_VOL = 'dsh-music-volume';
|
|
54
54
|
const PREF_PLAYBACK = 'dsh-music-playback';
|
|
55
|
-
const
|
|
55
|
+
const PREF_BOOKS_PLAYBACK = 'dsh-music-books-playback';
|
|
56
56
|
const PREF_ROOT = 'dsh-music-root';
|
|
57
57
|
const PREF_PANEL_POS = 'dsh-music-panel-pos';
|
|
58
58
|
const PREF_VOICE = 'dsh-music-voice';
|
|
59
59
|
const loadPref = (k) => { try { return localStorage.getItem(k); } catch (e) { return null; } };
|
|
60
60
|
const savePref = (k, v) => { try { localStorage.setItem(k, v); } catch (e) {} };
|
|
61
61
|
const clearPref = (k) => { try { localStorage.removeItem(k); } catch (e) {} };
|
|
62
|
+
// ---- per-book novel progress (independent from music) ----
|
|
63
|
+
// Every novel remembers its own position, keyed by its filename, so switching
|
|
64
|
+
// between books — or to music — never loses another book's place. Music
|
|
65
|
+
// progress lives in PREF_PLAYBACK and this subsystem never touches it.
|
|
66
|
+
const PREF_BOOK_PLAYBACK = 'dsh-music-book-playback'; // legacy single-book key
|
|
67
|
+
function readBooksPlayback() {
|
|
68
|
+
try {
|
|
69
|
+
const raw = loadPref(PREF_BOOKS_PLAYBACK);
|
|
70
|
+
if (raw) { const o = JSON.parse(raw); if (o && typeof o === 'object') return o; }
|
|
71
|
+
} catch (e) {}
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
function writeBooksPlayback(map) { savePref(PREF_BOOKS_PLAYBACK, JSON.stringify(map)); }
|
|
75
|
+
function getBookPlayback(name) { return readBooksPlayback()[name] || null; }
|
|
76
|
+
function clearBookPlayback(name) {
|
|
77
|
+
const map = readBooksPlayback();
|
|
78
|
+
if (Object.prototype.hasOwnProperty.call(map, name)) { delete map[name]; writeBooksPlayback(map); }
|
|
79
|
+
}
|
|
80
|
+
// Persist the currently playing novel's position into the per-book map.
|
|
81
|
+
function saveCurrentBookPlayback() {
|
|
82
|
+
const id = currentBookId();
|
|
83
|
+
if (id === null) return;
|
|
84
|
+
const book = bookById(id);
|
|
85
|
+
if (book === null) return;
|
|
86
|
+
const map = readBooksPlayback();
|
|
87
|
+
map[book.name] = {
|
|
88
|
+
from: bookFromRef, base: bookBaseTime,
|
|
89
|
+
pos: audio.currentTime || 0, total: bookTotal,
|
|
90
|
+
ts: Date.now(),
|
|
91
|
+
};
|
|
92
|
+
writeBooksPlayback(map);
|
|
93
|
+
}
|
|
94
|
+
// The most recently played novel (largest ts), used by refresh restore.
|
|
95
|
+
function latestBookPlayback() {
|
|
96
|
+
const map = readBooksPlayback();
|
|
97
|
+
let best = null, bestTs = -1;
|
|
98
|
+
for (const [name, e] of Object.entries(map)) {
|
|
99
|
+
if (e && typeof e.from === 'number' && e.ts > bestTs) { best = { name, ...e }; bestTs = e.ts; }
|
|
100
|
+
}
|
|
101
|
+
return best;
|
|
102
|
+
}
|
|
62
103
|
// Restore the playback-panel position ({x,y,h}) previously saved by dragging, if any.
|
|
63
104
|
function loadPanelPos() {
|
|
64
105
|
const raw = loadPref(PREF_PANEL_POS);
|
|
@@ -159,23 +200,28 @@ window.__ModuleLoader__.load({
|
|
|
159
200
|
if (typeof voice === 'string' && voice !== '') store.voice = voice;
|
|
160
201
|
} catch (e) {}
|
|
161
202
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}));
|
|
176
|
-
return;
|
|
203
|
+
// Migrate legacy single-book progress (pre-0.2.1) into the per-book map once.
|
|
204
|
+
try {
|
|
205
|
+
const legacy = loadPref(PREF_BOOK_PLAYBACK);
|
|
206
|
+
if (legacy) {
|
|
207
|
+
const p = JSON.parse(legacy);
|
|
208
|
+
if (p && typeof p.id === 'string' && typeof p.name === 'string' && p.name !== '') {
|
|
209
|
+
const map = readBooksPlayback();
|
|
210
|
+
if (!Object.prototype.hasOwnProperty.call(map, p.name)) {
|
|
211
|
+
map[p.name] = { from: p.from, base: p.base, pos: p.pos, total: p.total, ts: p.ts || Date.now() };
|
|
212
|
+
writeBooksPlayback(map);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
clearPref(PREF_BOOK_PLAYBACK);
|
|
177
216
|
}
|
|
178
|
-
|
|
217
|
+
} catch (e) {}
|
|
218
|
+
|
|
219
|
+
// Persist the current playback position. Music and novels are fully separate:
|
|
220
|
+
// music writes PREF_PLAYBACK, novels write into the per-book map — neither
|
|
221
|
+
// clears the other, so switching modes never loses progress.
|
|
222
|
+
function savePlayback() {
|
|
223
|
+
if (store.currentId === null) { clearPref(PREF_PLAYBACK); return; }
|
|
224
|
+
if (String(store.currentId).startsWith('book:')) { saveCurrentBookPlayback(); return; }
|
|
179
225
|
savePref(PREF_PLAYBACK, JSON.stringify({
|
|
180
226
|
id: store.currentId, name: store.currentName,
|
|
181
227
|
// While a restored track is still paused, the <audio> currentTime can
|
|
@@ -184,6 +230,7 @@ window.__ModuleLoader__.load({
|
|
|
184
230
|
// Persist the known duration too: on restore the browser may not have
|
|
185
231
|
// loaded the track's metadata yet, and we don't want a "0:00" readout.
|
|
186
232
|
duration: Number.isFinite(audio.duration) ? audio.duration : 0,
|
|
233
|
+
ts: Date.now(),
|
|
187
234
|
}));
|
|
188
235
|
}
|
|
189
236
|
function loadPlayback() {
|
|
@@ -581,7 +628,25 @@ window.__ModuleLoader__.load({
|
|
|
581
628
|
}
|
|
582
629
|
}
|
|
583
630
|
// `from` lets the toc jump straight to a chapter's chunk index.
|
|
584
|
-
function playBook(id, from = 0) { unlockAutoplay(); bookTotal = -1; bookBufferedFrom = -1; bookBaseTime = 0; bookRestorePos = -1;
|
|
631
|
+
function playBook(id, from = 0) { unlockAutoplay(); bookTotal = -1; bookBufferedFrom = -1; bookBaseTime = 0; bookRestorePos = -1; playBookFrom(id, from, false); saveCurrentBookPlayback(); }
|
|
632
|
+
// Play a novel from its saved progress when available (e.g. switching to
|
|
633
|
+
// music and back), otherwise start fresh from the beginning. Explicit
|
|
634
|
+
// chapter jumps keep using playBook(id, fromChunk) and are unaffected.
|
|
635
|
+
function resumeOrPlayBook(id) {
|
|
636
|
+
const book = bookById(id);
|
|
637
|
+
if (book === null) return;
|
|
638
|
+
const entry = getBookPlayback(book.name);
|
|
639
|
+
if (entry === null) { playBook(id); return; }
|
|
640
|
+
// Seed chunk / cumulative clock / in-chunk position from this book's entry.
|
|
641
|
+
restoreBookPlayback(book.name);
|
|
642
|
+
if (String(store.currentId) === 'book:' + id) {
|
|
643
|
+
// Restore applied: play the saved chunk; onTime seeks to the in-chunk pos.
|
|
644
|
+
unlockAutoplay();
|
|
645
|
+
playBookFrom(id, bookFromRef, false);
|
|
646
|
+
} else {
|
|
647
|
+
playBook(id); // restore bailed (book gone) → start fresh
|
|
648
|
+
}
|
|
649
|
+
}
|
|
585
650
|
// When a chunk ends, switch to the next HTTP chunk (warmed by preAudio).
|
|
586
651
|
// The switch is silent: no buffering flash, and the book-wide clock keeps
|
|
587
652
|
// the completed chunk's duration so the readout never resets.
|
|
@@ -745,6 +810,10 @@ window.__ModuleLoader__.load({
|
|
|
745
810
|
});
|
|
746
811
|
}
|
|
747
812
|
function stop() {
|
|
813
|
+
// Capture the current novel before resetting, so stopping forgets only
|
|
814
|
+
// that one book's position (other novels keep their own progress).
|
|
815
|
+
const stoppedBook = (store.currentId !== null && String(store.currentId).startsWith('book:'))
|
|
816
|
+
? bookById(currentBookId()) : null;
|
|
748
817
|
envReqId++;
|
|
749
818
|
trackEnv = null;
|
|
750
819
|
restoredMusicPos = null;
|
|
@@ -754,7 +823,7 @@ window.__ModuleLoader__.load({
|
|
|
754
823
|
stopBookHelper();
|
|
755
824
|
set({ currentId: null, currentName: null, playing: false, position: 0, duration: 0, pendingId: null, pendingName: null, vizState: 'ok', bookBuffering: false, bookError: '', bookBufferingSince: 0, bookBufferingSilent: false, tocOpen: false, currentSection: '', voiceSwitching: false });
|
|
756
825
|
clearPref(PREF_PLAYBACK);
|
|
757
|
-
|
|
826
|
+
if (stoppedBook !== null) clearBookPlayback(stoppedBook.name);
|
|
758
827
|
releaseWakeLock();
|
|
759
828
|
}
|
|
760
829
|
|
|
@@ -923,37 +992,41 @@ window.__ModuleLoader__.load({
|
|
|
923
992
|
restoredMusicPos = null;
|
|
924
993
|
}
|
|
925
994
|
// Save the restored spot explicitly so it survives another refresh.
|
|
926
|
-
savePref(PREF_PLAYBACK, JSON.stringify({ id: track.id, name: track.name, position: pos, duration: savedDur }));
|
|
995
|
+
savePref(PREF_PLAYBACK, JSON.stringify({ id: track.id, name: track.name, position: pos, duration: savedDur, ts: Date.now() }));
|
|
927
996
|
}
|
|
928
997
|
|
|
929
|
-
// Restore a novel's playback after a refresh
|
|
930
|
-
// cumulative clock, and same in-chunk position —
|
|
931
|
-
// matching how music is restored.
|
|
932
|
-
//
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
let
|
|
937
|
-
|
|
938
|
-
if (
|
|
939
|
-
|
|
998
|
+
// Restore a novel's playback after a refresh (or when resuming a book):
|
|
999
|
+
// same book, same chunk, same cumulative clock, and same in-chunk position —
|
|
1000
|
+
// paused (tap ▶ to resume), matching how music is restored. When targetName
|
|
1001
|
+
// is given, restore that book; otherwise restore the most recently played
|
|
1002
|
+
// novel from the per-book map.
|
|
1003
|
+
function restoreBookPlayback(targetName) {
|
|
1004
|
+
const map = readBooksPlayback();
|
|
1005
|
+
let name = targetName;
|
|
1006
|
+
let entry = null;
|
|
1007
|
+
if (typeof name === 'string' && name !== '') {
|
|
1008
|
+
entry = map[name] || null;
|
|
1009
|
+
} else {
|
|
1010
|
+
for (const [n, e] of Object.entries(map)) {
|
|
1011
|
+
if (e && typeof e.from === 'number' && (entry === null || e.ts > entry.ts)) { entry = e; name = n; }
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if (entry === null || !name) return;
|
|
1015
|
+
const book = store.books.find((b) => b.name === name);
|
|
940
1016
|
if (book === undefined) return; // the book is no longer in the library
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
const base = Number.isFinite(saved.base) ? saved.base : 0;
|
|
946
|
-
const pos = Number.isFinite(saved.pos) ? saved.pos : 0;
|
|
947
|
-
bookTotal = Number.isFinite(saved.total) ? saved.total : -1;
|
|
1017
|
+
const from = Number.isFinite(entry.from) && entry.from >= 0 ? entry.from : 0;
|
|
1018
|
+
const base = Number.isFinite(entry.base) ? entry.base : 0;
|
|
1019
|
+
const pos = Number.isFinite(entry.pos) ? entry.pos : 0;
|
|
1020
|
+
bookTotal = Number.isFinite(entry.total) ? entry.total : -1;
|
|
948
1021
|
bookFromRef = from;
|
|
949
1022
|
bookBaseTime = base;
|
|
950
|
-
const url = bookUrl(
|
|
1023
|
+
const url = bookUrl(book.id, from);
|
|
951
1024
|
if (url === null) return;
|
|
952
1025
|
// Mark the book as current. Like music restore, we do NOT touch the
|
|
953
1026
|
// <audio> element here (avoiding the Chromium 'getTopURL' quirk on
|
|
954
1027
|
// refresh); togglePlay loads + seeks the chunk when the user resumes.
|
|
955
1028
|
set({
|
|
956
|
-
currentId: 'book:' +
|
|
1029
|
+
currentId: 'book:' + book.id, currentName: book.name,
|
|
957
1030
|
position: base + pos, duration: base + (Number.isFinite(audio.duration) ? audio.duration : 0),
|
|
958
1031
|
pendingId: null, pendingName: null, error: null, playing: false,
|
|
959
1032
|
bookBuffering: false, bookBufferingSilent: false, bookError: '', bookBufferingSince: 0,
|
|
@@ -963,13 +1036,31 @@ window.__ModuleLoader__.load({
|
|
|
963
1036
|
// changed since the save); fall back to the saved total on failure.
|
|
964
1037
|
const savedTotal = bookTotal;
|
|
965
1038
|
bookTotal = -1;
|
|
966
|
-
void ensureBookTotal(
|
|
1039
|
+
void ensureBookTotal(book.id).then((total) => {
|
|
967
1040
|
if (!Number.isFinite(total) || total < 0) { bookTotal = savedTotal; return; }
|
|
968
1041
|
bookTotal = total;
|
|
969
|
-
if (from + 1 < total) preloadBook(
|
|
1042
|
+
if (from + 1 < total) preloadBook(book.id, from + 1);
|
|
970
1043
|
});
|
|
971
1044
|
}
|
|
972
1045
|
|
|
1046
|
+
// Restore whichever (music vs novel) was playing most recently — both now
|
|
1047
|
+
// persist independently, so a music interlude never wipes a novel's progress.
|
|
1048
|
+
function restoreLatest(list) {
|
|
1049
|
+
let musicTs = -1;
|
|
1050
|
+
try {
|
|
1051
|
+
const p = JSON.parse(loadPref(PREF_PLAYBACK) || 'null');
|
|
1052
|
+
if (p && typeof p.ts === 'number') musicTs = p.ts;
|
|
1053
|
+
} catch (e) {}
|
|
1054
|
+
const book = latestBookPlayback();
|
|
1055
|
+
const bookTs = book ? book.ts : -1;
|
|
1056
|
+
if (bookTs > musicTs) { restoreBookPlayback(); return; }
|
|
1057
|
+
if (musicTs > bookTs) { restorePlayback(list); return; }
|
|
1058
|
+
// tie / legacy data without timestamps: restore whatever exists (music
|
|
1059
|
+
// first, book last — legacy data only ever had one of the two populated).
|
|
1060
|
+
restorePlayback(list);
|
|
1061
|
+
restoreBookPlayback();
|
|
1062
|
+
}
|
|
1063
|
+
|
|
973
1064
|
// ---- host data ----
|
|
974
1065
|
async function loadTracks() {
|
|
975
1066
|
set({ loading: true });
|
|
@@ -994,8 +1085,7 @@ window.__ModuleLoader__.load({
|
|
|
994
1085
|
// Envelope (spectrum) decoding is deferred to actual playback — no need
|
|
995
1086
|
// to decode several full files eagerly at page load; the current track's
|
|
996
1087
|
// envelope decodes on play (startPlay / resume).
|
|
997
|
-
|
|
998
|
-
restoreBookPlayback();
|
|
1088
|
+
restoreLatest(list);
|
|
999
1089
|
} catch (err) {
|
|
1000
1090
|
set({ loading: false, error: '\u65e0\u6cd5\u8bfb\u53d6\u97f3\u4e50\u5e93\uff1a' + String((err && err.message) || err) });
|
|
1001
1091
|
}
|
|
@@ -1015,8 +1105,7 @@ window.__ModuleLoader__.load({
|
|
|
1015
1105
|
tracks: result.tracks || [], books: result.books || [],
|
|
1016
1106
|
count: result.count || 0, loading: false, error: null,
|
|
1017
1107
|
});
|
|
1018
|
-
|
|
1019
|
-
restoreBookPlayback();
|
|
1108
|
+
restoreLatest(result.tracks || []);
|
|
1020
1109
|
} else {
|
|
1021
1110
|
set({ loading: false, error: (result && result.error) || '\u8bbe\u7f6e\u76ee\u5f55\u5931\u8d25' });
|
|
1022
1111
|
}
|
|
@@ -1417,7 +1506,7 @@ window.__ModuleLoader__.load({
|
|
|
1417
1506
|
key: b.id,
|
|
1418
1507
|
className: 'dsh-music-track' + (active ? ' active' : ''),
|
|
1419
1508
|
title: b.url,
|
|
1420
|
-
onClick: () => { if (active) togglePlay(); else
|
|
1509
|
+
onClick: () => { if (active) togglePlay(); else resumeOrPlayBook(b.id); },
|
|
1421
1510
|
},
|
|
1422
1511
|
React.createElement('span', { className: 'dsh-music-track-name' }, (playing ? '\u25b6 ' : '') + b.name),
|
|
1423
1512
|
React.createElement('span', { className: 'dsh-music-track-size' }, formatSize(b.size)),
|
|
@@ -1609,7 +1698,7 @@ window.__ModuleLoader__.load({
|
|
|
1609
1698
|
const book = bookById(intent.id);
|
|
1610
1699
|
if (book !== null) {
|
|
1611
1700
|
set({ pendingId: 'book:' + book.id, pendingName: intent.name || book.name, error: null });
|
|
1612
|
-
|
|
1701
|
+
resumeOrPlayBook(book.id);
|
|
1613
1702
|
return;
|
|
1614
1703
|
}
|
|
1615
1704
|
const track = trackById(intent.id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-music-player",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "DeepSeek Harness 本地音乐 + AI 讲书插件:Host 扫描音乐目录并以 HTTP 流式提供音频、解析 .txt 小说结构并经 MiMo TTS 合成朗读,浏览器侧提供播放条/播放面板/章节目录跳转/多声音选择/实时频谱,并注册 music_play 模型工具",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|