Movies & TV: favorites, a family movie night list to vote on, and mixes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fe651a8c37
commit
2766424bb2
7 files changed
+107
-2
No files matched your search
+42
@@ -0,0 +1,42 @@
|
||||
// Movies & TV for the family: Favorites (each person's own hearted titles) and Movie night (one shared list everyone adds to and votes on, so the family picks together).
|
||||
// Titles are kept as a small card (Plex id, title, year, poster path), so the lists draw without asking Plex. Favorites: users/<id>/video-favs.json; Movie night: DATA_DIR/movienight.json.
|
||||
const fs = require('fs'), path = require('path');
|
||||
const atomicWrite = require('./atomic');
|
||||
const users = require('./users');
|
||||
const { readBody } = require('./body');
|
||||
|
||||
const NIGHT = () => path.join(process.env.DATA_DIR || path.join(__dirname, 'data'), 'movienight.json');
|
||||
const FAVS = () => path.join(users.dir(), 'video-favs.json');
|
||||
const rd = (f, d) => { try { const v = JSON.parse(fs.readFileSync(f, 'utf8')); return Array.isArray(v) ? v : d; } catch { return d; } };
|
||||
const wr = (f, v) => atomicWrite(f, JSON.stringify(v), { mode: 0o600 });
|
||||
const json = (res, code, o) => { res.writeHead(code, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(o)); };
|
||||
const fail = (code, message) => Object.assign(new Error(message), { code });
|
||||
const str = (v, n) => String(v == null ? '' : v).replace(/[\u0000-\u001f]/g, '').trim().slice(0, n);
|
||||
/** A title card from what the browser sent: only the fields a card needs, and the poster path must be a Plex path of ours. */
|
||||
function card(b) {
|
||||
const id = str(b.id, 20); if (!/^\d{1,12}$/.test(id)) throw fail(400, 'Pick a movie or show');
|
||||
const title = str(b.title, 160); if (!title) throw fail(400, 'That title has no name');
|
||||
return { id, title, year: Math.round(+b.year) || 0, type: b.type === 'show' ? 'show' : 'movie', thumb: /^\/library\/metadata\/\d+\/thumb(\/\d+)?$/.test(str(b.thumb, 120)) ? str(b.thumb, 120) : '' };
|
||||
}
|
||||
const nameOf = id => (users.get(id) || {}).name || 'Someone';
|
||||
const nightView = (l, me) => l.map(x => ({ ...x, byName: nameOf(x.by), votes: x.votes.length, voted: x.votes.includes(me.id), voters: x.votes.map(nameOf) })).sort((a, b) => b.votes - a.votes || a.added - b.added);
|
||||
|
||||
async function handle(req, res) {
|
||||
const me = users.current(), p = new URL(req.url, 'http://x').pathname.slice('/api/moviefam'.length) || '/';
|
||||
try {
|
||||
if (!me) throw fail(401, 'Sign in');
|
||||
if (p === '/night' && req.method === 'GET') return json(res, 200, { items: nightView(rd(NIGHT(), []), me) });
|
||||
if (p === '/favs' && req.method === 'GET') return json(res, 200, { items: rd(FAVS(), []) });
|
||||
if (req.method !== 'POST') throw fail(404, 'Not found');
|
||||
const b = JSON.parse(await readBody(req, 1e4) || '{}');
|
||||
if (p === '/fav') { const c = card(b), l = rd(FAVS(), []).filter(x => x.id !== c.id); if (b.on) l.unshift({ ...c, added: Date.now() }); wr(FAVS(), l.slice(0, 500)); return json(res, 200, { on: !!b.on }); }
|
||||
const l = rd(NIGHT(), []);
|
||||
if (p === '/night/add') { const c = card(b); if (l.some(x => x.id === c.id)) throw fail(400, c.title + ' is already on the list'); if (l.length >= 60) throw fail(400, 'The list is full; remove something first'); l.push({ ...c, by: me.id, added: Date.now(), votes: [me.id] }); wr(NIGHT(), l); return json(res, 200, { items: nightView(l, me) }); }
|
||||
if (p === '/night/clear') { if (me.role !== 'admin') throw fail(403, 'Only an admin can clear the list'); wr(NIGHT(), []); return json(res, 200, { items: [] }); }
|
||||
const x = l.find(y => y.id === str(b.id, 20)); if (!x) throw fail(404, 'That title is not on the list');
|
||||
if (p === '/night/vote') { x.votes = x.votes.includes(me.id) ? x.votes.filter(v => v !== me.id) : [...x.votes, me.id]; wr(NIGHT(), l); return json(res, 200, { items: nightView(l, me) }); }
|
||||
if (p === '/night/remove') { if (x.by !== me.id && me.role !== 'admin') throw fail(403, 'Only the person who added it can remove it'); wr(NIGHT(), l.filter(y => y !== x)); return json(res, 200, { items: nightView(l.filter(y => y !== x), me) }); }
|
||||
throw fail(404, 'Not found');
|
||||
} catch (e) { json(res, e.code && e.code < 600 ? e.code : 400, { error: e.message }); }
|
||||
}
|
||||
module.exports = { handle };
|
||||
Reference in new issue
Block a user