From e8b46bb3aa3d899b080e2efa59e50858da76d18e Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:48:27 -0400 Subject: [PATCH] Music: choose which Plex music library to browse Lists every music library on the server, remembers the choice per server, and shows it under the title with a picker. Co-Authored-By: Claude Sonnet 5.5 --- public/app.js | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/public/app.js b/public/app.js index 252297a..8ef5a32 100644 --- a/public/app.js +++ b/public/app.js @@ -536,7 +536,7 @@ const jfetch = async (path, opts) => { return j; }; const px = { - sec: null, + secs: null, async get(path) { const r = await fetch('/plex' + path, { headers: { Accept: 'application/json' } }); if (r.status === 401) throw Object.assign(new Error('Plex session expired'), { auth: true }); @@ -544,13 +544,19 @@ const px = { return (await r.json()).MediaContainer || {}; }, art: (thumb, w = 300) => thumb ? `/plex/photo/:/transcode?width=${w}&height=${w}&minSize=1&upscale=1&url=${encodeURIComponent(thumb)}` : 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', - async section() { - if (px.sec) return px.sec; + // All music libraries on the server; the chosen one is remembered per server. + async sections() { + if (px.secs) return px.secs; const d = (await px.get('/library/sections')).Directory || []; - const m = d.find(x => x.type === 'artist'); - if (!m) throw new Error('No music library found on this Plex server'); - return (px.sec = m.key); + px.secs = d.filter(x => x.type === 'artist').map(x => ({ key: x.key, title: x.title })); + if (!px.secs.length) { px.secs = null; throw new Error('No music library found on this Plex server'); } + return px.secs; }, + async section() { + const secs = await px.sections(), want = store.get('plex_lib', {})[px.serverKey]; + return (secs.find(x => x.key === want) || secs[0]).key; + }, + serverKey: '', }; const fmtDur = ms => { const s = Math.round((ms || 0) / 1000); return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); }; const mapTrack = (t, mc) => { @@ -606,7 +612,7 @@ async function plexPickServer() { ${esc(s.name)}${s.owned ? 'Yours' : 'Shared with you'}${icon('chev', 'chev')}`).join('')}
`; v.querySelectorAll('button.row').forEach(b => b.onclick = async () => { b.disabled = true; $('#pe').textContent = ''; - try { await jfetch('/api/plex/server', { method: 'POST', body: JSON.stringify({ id: b.dataset.id }) }); px.sec = null; route(); } + try { await jfetch('/api/plex/server', { method: 'POST', body: JSON.stringify({ id: b.dataset.id }) }); px.secs = null; route(); } catch (e) { b.disabled = false; $('#pe').textContent = e.message; // Offer a hand-typed address (e.g. the server's LAN address) when automatic discovery fails. @@ -615,7 +621,7 @@ async function plexPickServer() {
`; $('#pf2').onsubmit = async ev => { ev.preventDefault(); $('#pe').textContent = ''; - try { await jfetch('/api/plex/server', { method: 'POST', body: JSON.stringify({ id: b.dataset.id, uri: $('#pu').value.trim() }) }); px.sec = null; route(); } + try { await jfetch('/api/plex/server', { method: 'POST', body: JSON.stringify({ id: b.dataset.id, uri: $('#pu').value.trim() }) }); px.secs = null; route(); } catch (e2) { $('#pe').textContent = e2.message; } }; } @@ -624,14 +630,18 @@ async function plexPickServer() { } async function musicRoute([kind = 'albums', key], st) { - const v = $('#view'), sec = await px.section(); + px.serverKey = st.server; + const v = $('#view'), sec = await px.section(), secs = await px.sections(), cur = secs.find(x => x.key === sec) || secs[0]; const tabs = ['albums', 'artists', 'playlists', 'recent']; const seg = `
${tabs.map(t => `${t[0].toUpperCase() + t.slice(1)}`).join('')}
`; const card = (href, thumb, title, sub, round) => ` ${esc(title)}${esc(sub)}`; const listing = (items, html) => { - v.innerHTML = `

Music

${esc(st.server)}

${seg} + v.innerHTML = `

Music

${esc(st.server)}${secs.length > 1 ? ` · ${esc(cur.title)} ▾` : ''}

${seg}
${html || '

Nothing here

'}
`; + const pl2 = $('#plib'); + if (pl2) pl2.onclick = e => { e.preventDefault(); actionSheet('Music library', st.server, secs.map(x => ({ label: x.title + (x.key === cur.key ? ' ✓' : ''), icon: 'music', + run: () => { const m = store.get('plex_lib', {}); m[st.server] = x.key; store.set('plex_lib', m); location.hash = '#/music/albums'; route(); } }))); }; $('#mq').oninput = e => { const q = e.target.value.toLowerCase(); v.querySelectorAll('.acard').forEach(c => c.style.display = c.dataset.q.includes(q) ? '' : 'none'); }; }; const big = '&X-Plex-Container-Start=0&X-Plex-Container-Size=3000&sort=titleSort';