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 <noreply@anthropic.com>
This commit is contained in:
1 parent
f14a726f32
commit
e8b46bb3aa
1 file changed
+20
-10
+20
-10
@@ -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() {
|
||||
<span class="grow"><b>${esc(s.name)}</b><small>${s.owned ? 'Yours' : 'Shared with you'}</small></span>${icon('chev', 'chev')}</button>`).join('')}</div><div class="err" id="pe"></div><div id="pm"></div></div>`;
|
||||
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() {
|
||||
<div class="bar"><button class="btn">Try this address</button></div></form>`;
|
||||
$('#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 = `<div class="seg">${tabs.map(t => `<a href="#/music/${t}" class="${t === kind ? 'on' : ''}">${t[0].toUpperCase() + t.slice(1)}</a>`).join('')}</div>`;
|
||||
const card = (href, thumb, title, sub, round) => `<a class="acard ${round ? 'round' : ''}" href="${href}" data-q="${esc((title + ' ' + sub).toLowerCase())}">
|
||||
<img class="art" loading="lazy" alt="" src="${px.art(thumb)}"><b>${esc(title)}</b><small>${esc(sub)}</small></a>`;
|
||||
const listing = (items, html) => {
|
||||
v.innerHTML = `<div class="wrap"><h1>Music</h1><p class="sub">${esc(st.server)}</p>${seg}<input type="search" id="mq" placeholder="Filter" autocomplete="off">
|
||||
v.innerHTML = `<div class="wrap"><h1>Music</h1><p class="sub">${esc(st.server)}${secs.length > 1 ? ` · <a href="#" id="plib" style="color:var(--accent)">${esc(cur.title)} ▾</a>` : ''}</p>${seg}<input type="search" id="mq" placeholder="Filter" autocomplete="off">
|
||||
<div class="agrid" style="margin-top:14px">${html || '<p class="empty">Nothing here</p>'}</div></div>`;
|
||||
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';
|
||||
|
||||
Reference in new issue
Block a user