aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/music-app.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-28 15:35:20 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-28 15:35:58 +0200
commiteba2e6b14484c124f3c87ff95cd7ee640833e5d3 (patch)
treeba109b719c879df3ea40090e4fd6ff80b403a3a5 /packages/meshbay-hub/src/meshbay_hub/static/music-app.js
parente6917349c47cc4b7e04e1ecfab27c5291a5fe522 (diff)
downloadmeshbay-eba2e6b14484c124f3c87ff95cd7ee640833e5d3.tar.gz
feat(hub): cross-group search with reuse of existing views
Search page fetches indexes from all groups, then renders consolidated entries through the existing FilesPanel, VideoApp, and MusicApp components — no reimplemented views. Groups appear as top-level directories in the file browser; video/music entries use a synthetic root with per-entry transport refs for thumbnails and metadata across groups. Music player lifted to app.js with getConnection(groupId) for cross-group playback. Connection pool (max 3, LRU eviction) manages lazy WebRTC connections. All 10 locales updated with search keys. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/music-app.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/music-app.js39
1 files changed, 29 insertions, 10 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/music-app.js b/packages/meshbay-hub/src/meshbay_hub/static/music-app.js
index dd354d9..418da89 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/music-app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/music-app.js
@@ -174,8 +174,21 @@ function groupMusicEntries(entries, audioRoot) {
// -- album cover, MusicBrainz fetched lazily and only when actually needed --
+const _musicMetaRetryListeners = new Set();
+function bumpMusicMetaGeneration() {
+ for (const fn of _musicMetaRetryListeners) fn();
+}
+
function useMusicMeta(transportRef, fileId, active) {
const [meta, setMeta] = useState(null);
+ const [retryToken, setRetryToken] = useState(0);
+
+ useEffect(() => {
+ const listener = () => setRetryToken((n) => n + 1);
+ _musicMetaRetryListeners.add(listener);
+ return () => _musicMetaRetryListeners.delete(listener);
+ }, []);
+
useEffect(() => {
if (!active || !fileId) return;
let cancelled = false;
@@ -188,7 +201,7 @@ function useMusicMeta(transportRef, fileId, active) {
} catch { if (!cancelled) setMeta({ confidence: 0 }); }
})();
return () => { cancelled = true; };
- }, [fileId, active]);
+ }, [fileId, active, retryToken]);
return meta;
}
@@ -236,21 +249,24 @@ function DiscPlaceholder({ cls }) {
function AlbumCard({ album, transportRef, gekRef, musicbrainzEnabled, onOpen }) {
const repTrack = album.tracks.find((tr) => tr.thumb_hash) || album.tracks[0];
- // Only when nothing in the library already gives us a cover -- the common
- // case (a well-tagged rip with embedded art) needs no network call at all.
+ const tRef = repTrack._tRef || transportRef;
+ const gRef = repTrack._gRef || gekRef;
const needsLookup = musicbrainzEnabled && !repTrack.thumb_hash;
- const meta = useMusicMeta(transportRef, repTrack.id, needsLookup);
+ const meta = useMusicMeta(tRef, repTrack.id, needsLookup);
const coverHash = repTrack.thumb_hash || (meta && meta.cover_thumb_hash) || null;
return html`
<div class="music-card" onClick=${onOpen}>
${coverHash
? html`<${MediaThumb} thumbHash=${coverHash} alt=${album.album}
- cls="music-cover" transportRef=${transportRef} gekRef=${gekRef} />`
+ cls="music-cover" transportRef=${tRef} gekRef=${gRef} />`
: html`<${DiscPlaceholder} cls="music-cover" />`}
<div class="music-card-info">
<div class="music-card-title">${album.album}</div>
<div class="music-card-sub">${album.artist}</div>
+ ${repTrack.groupName && html`
+ <div class="music-card-group">${repTrack.groupName}</div>
+ `}
</div>
</div>
`;
@@ -260,8 +276,10 @@ function AlbumCard({ album, transportRef, gekRef, musicbrainzEnabled, onOpen })
function MusicDetailModal({ album, transportRef, gekRef, musicbrainzEnabled, onClose, onPlayQueue }) {
const repTrack = album.tracks.find((tr) => tr.thumb_hash) || album.tracks[0];
+ const tRef = repTrack._tRef || transportRef;
+ const gRef = repTrack._gRef || gekRef;
const needsLookup = musicbrainzEnabled && !repTrack.thumb_hash;
- const meta = useMusicMeta(transportRef, repTrack.id, needsLookup);
+ const meta = useMusicMeta(tRef, repTrack.id, needsLookup);
const coverHash = repTrack.thumb_hash || (meta && meta.cover_thumb_hash) || null;
return html`
@@ -278,7 +296,7 @@ function MusicDetailModal({ album, transportRef, gekRef, musicbrainzEnabled, onC
<div class="music-detail-header">
${coverHash
? html`<${MediaThumb} thumbHash=${coverHash} alt=${album.album}
- cls="music-detail-cover" transportRef=${transportRef} gekRef=${gekRef} />`
+ cls="music-detail-cover" transportRef=${tRef} gekRef=${gRef} />`
: html`<${DiscPlaceholder} cls="music-detail-cover" />`}
<div class="music-detail-meta">
<div class="music-detail-artist">${album.artist}</div>
@@ -437,6 +455,7 @@ function FlatList({ tracks, artists, onPlayQueue }) {
function MusicApp({
groupId, transportRef, gekRef, status, entries, audioRoot, musicbrainzConfig, onPlayQueue,
+ hideFilter,
}) {
const [mode, setMode] = useState(loadViewMode);
const [filter, setFilter] = useState('');
@@ -487,11 +506,11 @@ function MusicApp({
onClick=${() => setModeAndSave('flat')}>
${t('music.mode_flat')}
</button>
- <div class="tb-search">
+ ${!hideFilter && html`<div class="tb-search">
<${Icon} name="search" />
<input type="text" placeholder="${t('group.filter')}"
value=${filter} onInput=${(e) => setFilter(e.target.value)} />
- </div>
+ </div>`}
</div>
${empty && html`<p class="page-message">${t('music.empty')}</p>`}
${!empty && needle && filteredArtists.length === 0 && filteredTracks.length === 0 && html`
@@ -506,4 +525,4 @@ function MusicApp({
`;
}
-export { MusicApp };
+export { MusicApp, groupMusicEntries, bumpMusicMetaGeneration };