Plex playlists you can edit (create, add, remove, rename, delete, save queue), sleep timer

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-22 07:52:29 -04:00
1 parent fa1452d23d
commit c8399ccaa7
5 files changed
+102 -12

No files matched your search

+6 -4
View File
@@ -99,7 +99,7 @@ function serveStatic(req, res) {
if (err) { res.writeHead(404); return res.end('Not found'); }
// The pages link to /app.js?v=<build> etc. A new release changes the address, so no proxy cache ("Cache Assets" in
// Nginx Proxy Manager, a CDN, the browser) can hand out an old copy.
if (p === '/index.html' || p === '/login.html') data = Buffer.from(String(data).replace(/(src|href)="(\/(?:app|webauthn|login|links|palette|shell|calendar-views)\.js|\/style\.css)"/g, `$1="$2?v=${BUILD}"`));
if (p === '/index.html' || p === '/login.html') data = Buffer.from(String(data).replace(/(src|href)="(\/(?:app|webauthn|login|links|palette|shell|calendar-views|music-extras)\.js|\/style\.css)"/g, `$1="$2?v=${BUILD}"`));
const headers = { 'content-type': TYPES[path.extname(file)] || 'application/octet-stream' };
if (p.startsWith('/icons/')) headers['cache-control'] = 'public, max-age=604800'; // icons and splash screens rarely change
else { // everything else: the browser may keep a copy but must check with us first, so a new version is never missed
@@ -113,7 +113,7 @@ function serveStatic(req, res) {
}
// Changes whenever the app's files change, so an open page can tell that a newer version is available.
const BUILD = (() => { try { return crypto.createHash('sha1').update(['app.js', 'links.js', 'palette.js', 'shell.js', 'calendar-views.js', 'index.html', 'style.css', 'webauthn.js', 'login.js', 'login.html', 'sw.js'].map(f => fs.readFileSync(path.join(PUBLIC, f))).join('')).digest('hex').slice(0, 12); } catch { return String(Date.now()); } })();
const BUILD = (() => { try { return crypto.createHash('sha1').update(['app.js', 'links.js', 'palette.js', 'shell.js', 'calendar-views.js', 'music-extras.js', 'index.html', 'style.css', 'webauthn.js', 'login.js', 'login.html', 'sw.js'].map(f => fs.readFileSync(path.join(PUBLIC, f))).join('')).digest('hex').slice(0, 12); } catch { return String(Date.now()); } })();
// ---- OnlyOffice in-app editing (JWT-signed editor config + save-back callback to Seafile)
const JWT_SECRET = process.env.ONLYOFFICE_JWT_SECRET || '';
@@ -323,6 +323,8 @@ async function plexApi(req, res) {
} catch (e) { json(res, 502, { error: e.message }); }
}
// Only playlists may be changed through the proxy (create, add, remove, rename, delete); everything else on the Plex server stays read-only.
const PLEX_WRITABLE = /^\/plex\/playlists(\/\d+)?(\/items(\/\d+)?)?\?|^\/plex\/playlists(\/\d+)?(\/items(\/\d+)?)?$/;
async function plexProxy(req, res) {
const st = plexLoad();
if (!st.srv || !st.stk) { res.writeHead(401); return res.end(); }
@@ -330,7 +332,7 @@ async function plexProxy(req, res) {
res.on('close', () => ac.abort());
// A server that accepts the connection but never answers must not leave the player waiting forever: give up after 15 s without a response.
const attempt = async s => { const hc = new AbortController(), t = setTimeout(() => hc.abort(), 15000); try { return await tryFetch(s, AbortSignal.any([ac.signal, hc.signal])); } finally { clearTimeout(t); } };
const tryFetch = (s, signal) => fetch(s.srv + req.url.slice('/plex'.length), { signal,
const tryFetch = (s, signal) => fetch(s.srv + req.url.slice('/plex'.length), { signal, method: req.method,
headers: { ...PLEX_HEADERS, 'X-Plex-Token': s.stk, Accept: req.headers.accept || 'application/json', 'Accept-Encoding': 'identity', ...(req.headers.range ? { range: req.headers.range } : {}) } });
let up = null;
try { up = await attempt(st); } catch { up = null; }
@@ -526,7 +528,7 @@ async function handleRequest(req, res) {
if (pathname.startsWith('/api/signal/')) return signal.enabled() ? signal.handle(req, res) : forbidden(res, 'Signal is not set up on this server');
if (WEBMAIL_BACKEND_URL && isWebmailPath(pathname)) return proxyWebmail(req, res);
if (req.url.startsWith('/api/plex/')) return plexApi(req, res);
if (req.url.startsWith('/plex/') && req.method === 'GET') return plexProxy(req, res);
if (req.url.startsWith('/plex/') && (req.method === 'GET' || (['POST', 'PUT', 'DELETE'].includes(req.method) && PLEX_WRITABLE.test(req.url)))) return plexProxy(req, res);
if (req.url.startsWith('/sf/')) return proxySeafile(req, res);
return serveStatic(req, res);
});