'use strict';
// ---- Plex Music (via the server's /plex proxy; sign-in goes through plex.tv, no password handled here)
const jfetch = async (path, opts) => {
const r = await fetch(path, opts); const j = await r.json().catch(() => ({}));
if (!r.ok) throw Object.assign(new Error(j.error || 'Request failed (' + r.status + ')'), { status: r.status });
return j;
};
const px = {
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 });
if (!r.ok) throw new Error('Plex error ' + r.status);
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==',
// 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 || [];
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) => {
const part = t.Media?.[0]?.Part?.[0];
return part && { key: t.ratingKey, title: t.title, artist: t.grandparentTitle || t.originalTitle || mc.title1 || '',
album: t.parentTitle || mc.title2 || '', thumb: t.thumb || t.parentThumb || mc.thumb, src: '/plex' + part.key, dur: t.duration || 0 };
};
async function viewMusic(args) {
const v = $('#view');
v.innerHTML = '
Music
Loading…
';
try {
const st = await jfetch('/api/plex/status');
if (!location.hash.startsWith('#/music')) return;
if (!st.signedIn) return plexSignIn();
if (!st.server) return plexPickServer();
await musicRoute(args, st);
renderNpCard();
} catch (e) {
if (e.auth) return plexSignIn();
v.innerHTML = `
Music
${esc(e.message)}
`;
$('#rs').onclick = plexPickServer;
}
}
// The sign-in on plex.tv may take over this very tab (installed apps, some browsers) and then come back, so the pending sign-in is remembered
// and picked up again when this page loads.
const PLEX_PIN = 'plex_pin';
async function plexWaitPin(pin, w) {
for (let i = 0; i < 150; i++) {
await new Promise(r => setTimeout(r, 2000));
if (!location.hash.startsWith('#/music') && !location.hash.startsWith('#/video')) return;
const r = await jfetch(`/api/plex/pin?id=${pin.id}&code=${encodeURIComponent(pin.code)}`);
if (r.ok) { try { localStorage.removeItem(PLEX_PIN); } catch {} try { w && w.close(); } catch {} return route(); }
}
try { localStorage.removeItem(PLEX_PIN); } catch {}
const e = $('#pe'); if (e) e.textContent = 'Timed out waiting for Plex. Try again.';
}
function plexSignIn(msg = '') {
$('#view').innerHTML = `
Music
Play your Plex music library here.
You'll sign in on plex.tv in a new tab. This app never sees your Plex password.
${esc(msg)}
`;
let pend = null; try { pend = JSON.parse(localStorage.getItem(PLEX_PIN)); } catch {}
if (pend && Date.now() - pend.t < 10 * 60e3) { $('#pe').textContent = 'Finishing your Plex sign-in…'; plexWaitPin(pend, null).catch(e => { const x = $('#pe'); if (x) x.textContent = e.message; }); }
$('#pl').onclick = async () => {
const w = window.open('', '_blank'); // opened synchronously so popup blockers allow it
try {
const pin = await jfetch('/api/plex/pin', { method: 'POST' });
try { localStorage.setItem(PLEX_PIN, JSON.stringify({ id: pin.id, code: pin.code, t: Date.now() })); } catch {}
if (w) w.location = pin.authUrl; else $('#pe').innerHTML = `Tap to open Plex sign-in`;
await plexWaitPin(pin, w);
} catch (e) { $('#pe').textContent = e.message; }
};
}
async function plexPickServer() {
const v = $('#view');
v.innerHTML = '