Music: use Plex as another person in your Plex Home (list, PIN, switch back), shown as Playing as <name>
build-container / build (push) Successful in 19m30s
build-container / build (push) Successful in 19m30s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cedd6845ce
commit
dae19176cd
6 files changed
+132
-10
No files matched your search
@@ -223,6 +223,21 @@ const plexTv = async (path, token, opts = {}) => {
|
||||
return r.json();
|
||||
};
|
||||
const RESOURCES = '/api/v2/resources?includeHttps=1&includeRelay=1';
|
||||
/** plex.tv's Home endpoints answer XML or JSON depending on the call; read either into plain objects. */
|
||||
const attrs = tag => Object.fromEntries([...tag.matchAll(/([\w:-]+)="([^"]*)"/g)].map(m => [m[1], m[2].replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>')]));
|
||||
async function plexTvAny(path, token, opts = {}) {
|
||||
const r = await fetch(PLEX_TV + path, { ...opts, headers: { ...PLEX_HEADERS, ...(token ? { 'X-Plex-Token': token } : {}) } });
|
||||
const text = await r.text();
|
||||
if (!r.ok) { const e = new Error('plex.tv ' + r.status); e.status = r.status; e.body = text; throw e; }
|
||||
try { return { json: JSON.parse(text), text }; } catch { return { json: null, text }; }
|
||||
}
|
||||
const truthy = v => v === true || v === 1 || v === '1' || v === 'true';
|
||||
/** The people in this Plex Home: [{ id, title, thumb, admin, protected, restricted, guest }]. */
|
||||
async function plexHomeUsers(token) {
|
||||
const { json, text } = await plexTvAny('/api/home/users', token);
|
||||
const raw = json ? (json.users || json.MediaContainer?.User || json.User || []) : [...text.matchAll(/<User\b[^>]*>/g)].map(m => attrs(m[0]));
|
||||
return raw.map(u => ({ id: String(u.id), title: u.title || u.username || u.friendlyName || 'User', thumb: u.thumb || '', admin: truthy(u.admin), protected: truthy(u.protected) || truthy(u.hasPassword), restricted: truthy(u.restricted), guest: truthy(u.guest) }));
|
||||
}
|
||||
|
||||
/** Find a working address for one Plex server. Candidates, best first: its LAN address over plain http (no DNS, works from a
|
||||
* container on the same network), then its plex.direct names, then the public address, then Plex's relay.
|
||||
@@ -296,7 +311,7 @@ async function plexApi(req, res) {
|
||||
const u = new URL(req.url, 'http://x'), st = plexLoad(), token = st.token;
|
||||
try {
|
||||
if (u.pathname === '/api/plex/status') {
|
||||
return json(res, 200, { signedIn: !!token, server: st.srv ? st.name || 'Plex' : null });
|
||||
return json(res, 200, { signedIn: !!token, server: st.srv ? st.name || 'Plex' : null, user: st.user ? { id: st.user.id, title: st.user.title, thumb: st.user.thumb, admin: st.user.admin } : null });
|
||||
}
|
||||
if (u.pathname === '/api/plex/pin' && req.method === 'POST') {
|
||||
const pin = await plexTv('/api/v2/pins?strong=true', null, { method: 'POST' });
|
||||
@@ -329,6 +344,39 @@ async function plexApi(req, res) {
|
||||
console.log(`[plex] connected to ${r.name} (${c.label})`);
|
||||
return json(res, 200, { ok: true, name: r.name, via: c.label });
|
||||
}
|
||||
if (u.pathname === '/api/plex/users' && req.method === 'GET') { // who is in this Plex Home, and who is using NE Hub right now
|
||||
let users; try { users = await plexHomeUsers(st.mainToken || token); } catch (e) { return json(res, 200, { users: [], current: st.user || null, note: e.status === 404 ? 'This Plex account has no Plex Home.' : 'Could not read the Plex Home: ' + e.message }); }
|
||||
const cur = st.user ? st.user.id : (users.find(x => x.admin) || {}).id;
|
||||
return json(res, 200, { users: users.map(x => ({ ...x, current: x.id === cur })), current: st.user || null });
|
||||
}
|
||||
if (u.pathname === '/api/plex/switch' && req.method === 'POST') { // use Plex as another person in the Home (their PIN if they have one)
|
||||
if (!st.sid) return json(res, 400, { error: 'Choose a Plex server first' });
|
||||
const { id, pin } = JSON.parse(await readBody(req) || '{}');
|
||||
const owner = st.mainToken || token;
|
||||
const target = (await plexHomeUsers(owner)).find(x => x.id === String(id));
|
||||
if (!target) return json(res, 404, { error: 'That person is not in your Plex Home' });
|
||||
if (target.admin && !st.mainToken) { plexSave({ ...st, user: { id: target.id, title: target.title, thumb: target.thumb, admin: true } }); return json(res, 200, { ok: true, user: { id: target.id, title: target.title, thumb: target.thumb, admin: true } }); } // already the account that signed in
|
||||
let userToken;
|
||||
if (target.admin && st.mainToken) userToken = st.mainToken; // back to the account that signed in: no PIN needed
|
||||
else {
|
||||
try {
|
||||
const q = pin ? '?' + new URLSearchParams({ pin: String(pin).slice(0, 8) }) : '';
|
||||
const { json: j, text } = await plexTvAny(`/api/home/users/${encodeURIComponent(target.id)}/switch${q}`, owner, { method: 'POST' });
|
||||
userToken = (j && (j.authToken || j.authenticationToken || (j.user && (j.user.authToken || j.user.authenticationToken)))) || (text.match(/authenticationToken="([^"]+)"/) || [])[1];
|
||||
} catch (e) {
|
||||
if (e.status === 401 || e.status === 403) return json(res, 403, { error: target.protected || pin ? 'That PIN is not right' : 'Plex would not switch to that person' });
|
||||
throw e;
|
||||
}
|
||||
if (!userToken) return json(res, 502, { error: 'Plex did not give a sign-in for that person' });
|
||||
}
|
||||
const rs = await plexTv(RESOURCES, userToken), r = rs.find(x => x.clientIdentifier === st.sid);
|
||||
if (!r) return json(res, 403, { error: target.title + ' cannot use this Plex server' });
|
||||
const c = await plexConnect(r);
|
||||
if (!c.ok) return json(res, 502, { error: c.summary });
|
||||
plexSave({ ...st, mainToken: st.mainToken || (target.admin ? undefined : token), token: userToken, srv: c.uri, stk: r.accessToken, user: { id: target.id, title: target.title, thumb: target.thumb, admin: target.admin } });
|
||||
console.log(`[plex] now using Plex as ${target.title}`);
|
||||
return json(res, 200, { ok: true, user: { id: target.id, title: target.title, thumb: target.thumb, admin: target.admin } });
|
||||
}
|
||||
if (u.pathname === '/api/plex/logout' && req.method === 'POST') {
|
||||
plexWipe();
|
||||
return json(res, 200, { ok: true });
|
||||
|
||||
Reference in new issue
Block a user