diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-24 17:39:41 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-24 17:39:41 +0200 |
| commit | 8c780b9928db8145b7fe18ecd137384ccfe25d8f (patch) | |
| tree | 5cfc06eec0fdf9bb7fe0a14f45e3ef803bb86a20 /packages/meshbay-hub/src/meshbay_hub/static/group-settings.js | |
| parent | 941d1a135dd7b03834576855e8e9fdaa24c4e406 (diff) | |
| download | meshbay-8c780b9928db8145b7fe18ecd137384ccfe25d8f.tar.gz | |
feat(hub): Music app client — album grid, flat list, persistent player
Implements the client half of docs/musicbay.md against MNP 0.8:
- music-app.js: album grid (grouped by artist -> album, from index-time
artist/album fields) or flat folder view, per-group localStorage
toggle like Videos. MusicBrainz (music_meta_req) is only looked up
when a track has no embedded cover at all — well-tagged files never
trigger a network call, unlike Videos where TMDB is unconditional.
Reuses video-app.js's MediaThumb/LazyTile (now exported) rather than
duplicating the chunk-path thumbnail decode + virtualization.
- music-player.js: the persistent player bar — queue, shuffle (Fisher-
Yates, keeps the current track in place when toggled), repeat (off/
all/one), volume (localStorage), prev/next, a one-track prefetch
cache. No MSE, no node-side streaming: a track is downloaded and
decrypted once via file-utils.js's pipelinedDownload, same chunk
pipeline Files already uses, then played from a blob URL.
- group-page.js: owns musicQueue/musicbrainzConfig state and renders
MusicPlayerBar outside the tab-switched area — deliberately, so
playback survives navigating to Chat/Files, the same reasoning the
video/preview modals are shell-owned rather than app-owned.
- apps.js: registers "music". transport.js: fetchMusicMeta (keyed by
path, same reordering-hazard fix as fetchMediaMeta),
setMusicbrainzConfig/setMusicbrainzEnabled (signed ops, mirroring
TMDB's), and the three new ack handlers. group-settings.js: a
MusicBrainz settings section (contact string, per-group toggle) —
the existing Applications checklist already picks up "music" for
free, per apps.md's own claim.
- icon.js: music/pause/skip-next/skip-prev/shuffle/repeat/volume,
drawn in the same stroked style as the existing set.
- i18n: group.tab_music, the music.* and settings_node.musicbrainz_*
keys, translated (not just copied) across all ten locales, Polish
carrying full one/few/many/other plural forms for music.n_tracks.
- webapp.py's _ASSETS, test_hook_ordering.py's STATIC_FILES and
test_transport_contracts.py's SPLIT_FILES gain the two new files.
Full suite (common + hub + node): 1116 passed, no regressions.
`npm run sync-ui` in meshbay-client confirmed both files copied.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBi7ALLGfwcjBXt57yNMcy
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/group-settings.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/group-settings.js | 106 |
1 files changed, 106 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js index 88ab68e..03162d9 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js @@ -34,6 +34,7 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, enabledApps, onEnabledApps, scanSettings, onScanSettings, tmdbConfig, onTmdbConfig, onTmdbEnabled, + musicbrainzConfig, onMusicbrainzConfig, onMusicbrainzEnabled, entries, nodeDirs, videoRoot, onVideoRoot, onPaired, onLeft }) { const [members, setMembers] = useState([]); @@ -358,6 +359,74 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, saveTmdbConfig(); }, [isNodeAdmin, connected, tmdbConfig, saveTmdbConfig]); + const [mbBusy, setMbBusy] = useState(false); + const [mbMsg, setMbMsg] = useState(''); + const [mbContactDraft, setMbContactDraft] = useState(''); + const [mbEnabledBusy, setMbEnabledBusy] = useState(false); + const mbEnabled = musicbrainzConfig ? musicbrainzConfig.enabled : true; + + /** + * Whether MusicBrainz is used at all — per-group from the start + * (docs/musicbay.md §3.2/§6). Same shape as saveTmdbEnabled. + */ + const saveMusicbrainzEnabled = useCallback(async (nextEnabled) => { + const transport = transportRef && transportRef.current; + setMbMsg(''); + setMbEnabledBusy(true); + try { + if (!transport || !transport.connected) { + throw new Error('Not connected to the node'); + } + const sk = transport.sessionKeys && transport.sessionKeys.skEdB64; + const signFn = (sk && window.MeshBayKeys) + ? (transcript) => window.MeshBayKeys.signBytes(sk, transcript) + : null; + await transport.setMusicbrainzEnabled(nextEnabled, signFn); + if (onMusicbrainzEnabled) onMusicbrainzEnabled(nextEnabled); + } catch (err) { + setMbMsg(err.message); + } finally { + setMbEnabledBusy(false); + } + }, [transportRef, onMusicbrainzEnabled]); + + /** + * The node-wide MusicBrainz contact string (docs/musicbay.md §3.2) — not + * a secret, unlike TMDB's token, but still cleared from the draft field + * after a save: the node never echoes it back + * (musicbrainz_config_ack carries only whether one is set), so there is + * nothing to keep showing. + */ + const saveMusicbrainzConfig = useCallback(async () => { + const transport = transportRef && transportRef.current; + setMbMsg(''); + setMbBusy(true); + try { + if (!transport || !transport.connected) { + throw new Error('Not connected to the node'); + } + const sk = transport.sessionKeys && transport.sessionKeys.skEdB64; + const signFn = (sk && window.MeshBayKeys) + ? (transcript) => window.MeshBayKeys.signBytes(sk, transcript) + : null; + const contact = mbContactDraft.trim(); + await transport.setMusicbrainzConfig(contact || undefined, signFn); + setMbContactDraft(''); + if (onMusicbrainzConfig) { + onMusicbrainzConfig({ + contactConfigured: contact + ? true + : (musicbrainzConfig ? musicbrainzConfig.contactConfigured : false), + }); + } + setMbMsg(t('settings_node.scan_saved')); + } catch (err) { + setMbMsg(err.message); + } finally { + setMbBusy(false); + } + }, [transportRef, onMusicbrainzConfig, mbContactDraft, musicbrainzConfig]); + // Every folder anywhere in the group's shared index, deepest included — // `entries[].path` is each file's containing directory (files-app.js's own // convention), so every ancestor prefix of it is a real folder, and @@ -688,6 +757,43 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, </div> `} + ${/* Same two-part shape as TMDB above: the on/off switch is per-group, + the contact string stays node-wide (docs/musicbay.md §3.2) — + one operator identity, not a per-group concern. Unlike TMDB + there is no token field: MusicBrainz's read endpoints need no + credential, just a descriptive User-Agent contact. */ + isNodeAdmin && connected && html` + <div class="settings-section"> + <h3 class="settings-heading">${t('settings_node.musicbrainz_title')}</h3> + <p class="settings-hint">${t('settings_node.musicbrainz_hint')}</p> + <div class="settings-row"> + <label class="settings-label"> + <input type="checkbox" checked=${mbEnabled} disabled=${mbEnabledBusy} + onChange=${(e) => saveMusicbrainzEnabled(e.target.checked)} /> + ${' '}${mbEnabled ? t('settings_node.musicbrainz_enabled') : t('settings_node.musicbrainz_disabled')} + </label> + </div> + <div class="settings-row"> + <label class="settings-label"> + ${t('settings_node.musicbrainz_contact_label')} + <input type="text" placeholder=${t('settings_node.musicbrainz_contact_placeholder')} + value=${mbContactDraft} disabled=${mbBusy} + onInput=${e => setMbContactDraft(e.target.value)} /> + </label> + <p class="settings-hint"> + ${musicbrainzConfig && musicbrainzConfig.contactConfigured + ? t('settings_node.musicbrainz_contact_set') + : t('settings_node.musicbrainz_contact_unset')} + </p> + </div> + <button class="btn btn-small btn-secondary" style="margin-top:8px" + disabled=${mbBusy} onClick=${() => saveMusicbrainzConfig()}> + ${mbBusy ? t('settings_node.scan_saving') : t('settings_node.musicbrainz_save')} + </button> + ${mbMsg && html`<p class="settings-hint">${mbMsg}</p>`} + </div> + `} + ${/* Which folder is the Videos app's entry point for this group — per-group like uploads, not node-wide like TMDB (mediacenter.md §5.6). Until one is chosen, the Videos tab says so instead of |