Calendar: several NE Hub calendars with colors, per-person view/edit sharing and per-calendar subscribe links; Movies & TV: converted video keeps the right time and seeks inside what the stream has

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-22 12:38:15 -04:00
1 parent d25eae5920
commit d813884074
8 files changed
+343 -46

No files matched your search

+148 -27
View File
@@ -222,60 +222,173 @@ function expand(events, win) { // win: { from, to (ms), fd, td (local YYYY-MM-DD
return out;
}
// ---- The NE Hub calendar: events created in the app, stored here, and published as a subscribe feed.
const EV_FILE = 'events.json', FEED_FILE = 'feed.json';
// ---- NE Hub calendars: any number per person, stored here, shared by choice, and published as subscribe feeds.
const EV_FILE = 'events.json', FEED_FILE = 'feed.json', NCAL_FILE = 'ncals.json', COLOR_FILE = 'calcolors.json';
const readJ = (f, d) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return d; } };
const writeJ = (f, v) => atomicWrite(here(f), JSON.stringify(v, null, 2), { mode: 0o600 });
const loadLocal = () => readJ(here(EV_FILE), []);
const LOCAL = { id: 'local', name: 'NE Hub', color: '#4f7cff' };
const HEX = /^#[0-9a-f]{6}$/i;
const DEFAULT_COLOR = '#4f7cff', COLORS = [DEFAULT_COLOR, '#2f9e6e', '#e0742b', '#d9453a', '#8a5cf0', '#e5a00d'];
const dirFor = id => users.dirOf(id);
const myId = () => users.current().id;
/** A person's own NE Hub calendars. The first one is the original "NE Hub" calendar (id "local"), so older events stay in it. */
function calsIn(dir) {
const l = readJ(path.join(dir, NCAL_FILE), null);
return Array.isArray(l) && l.length ? l : [{ id: 'local', name: 'NE Hub', color: DEFAULT_COLOR, shares: [] }];
}
const saveCals = v => writeJ(NCAL_FILE, v);
const eventsIn = dir => readJ(path.join(dir, EV_FILE), []);
const saveEvents = (dir, v) => atomicWrite(path.join(dir, EV_FILE), JSON.stringify(v, null, 2), { mode: 0o600 });
const evCal = e => e.cal || 'local';
const colorOverrides = () => readJ(here(COLOR_FILE), {});
/** Calendars other people shared with the current person: [{ id: "owner~calId", owner, ownerName, cal, mode }] */
function sharedWithMe() {
const me = myId(), out = [];
for (const u of users.all()) {
if (u.disabled || u.id === me) continue;
for (const c of calsIn(dirFor(u.id))) {
const sh = (c.shares || []).find(x => x.user === me);
if (sh) out.push({ id: u.id + '~' + c.id, owner: u.id, ownerName: u.name, cal: c, mode: sh.mode === 'edit' ? 'edit' : 'view' });
}
}
return out;
}
/** Find the NE Hub calendar an id points at, with the current person's rights on it (null if none). */
function resolveCal(id) {
const me = myId();
if (typeof id === 'string' && id.includes('~')) {
const [owner, cid] = id.split('~');
const u = users.get(owner);
if (!u || u.disabled) return null;
const c = calsIn(dirFor(owner)).find(x => x.id === cid);
const sh = c && (c.shares || []).find(x => x.user === me);
return sh ? { owner, dir: dirFor(owner), cal: c, cid, mode: sh.mode === 'edit' ? 'edit' : 'view' } : null;
}
const c = calsIn(dirFor(me)).find(x => x.id === id);
return c ? { owner: me, dir: dirFor(me), cal: c, cid: c.id, mode: 'owner' } : null;
}
const canWrite = r => r && (r.mode === 'owner' || r.mode === 'edit');
const icsOf = (dir, ids) => ics.buildCalendar(eventsIn(dir).filter(e => !ids || ids.includes(evCal(e))).map(e => ({ uid: e.uid, fields: e.fields })));
const localIcs = () => icsOf(dirFor(myId()));
function feedToken(reset) {
let t = readJ(here(FEED_FILE), {}).token;
if (!t || reset) { t = crypto.randomBytes(24).toString('hex'); writeJ(FEED_FILE, { token: t }); }
return t;
}
const localIcs = () => ics.buildCalendar(loadLocal().map(e => ({ uid: e.uid, fields: e.fields })));
function calToken(id, reset) {
const cals = calsIn(users.dir()), c = cals.find(x => x.id === id);
if (!c) throw new Error('Calendar not found');
if (!c.token || reset) { c.token = crypto.randomBytes(24).toString('hex'); saveCals(cals); }
return c.token;
}
/** GET /cal/feed/<token>.ics : public on purpose (Outlook/Google/Apple fetch it without a session); the token is the secret.
* Every person has their own token, and it only ever shows that person's own NE Hub events. */
* A person's own token shows all their NE Hub calendars; a calendar's token shows just that one. */
function feed(req, res) {
const m = /^\/cal\/feed\/([a-f0-9]+)\.ics$/.exec(new URL(req.url, 'http://x').pathname);
const got = Buffer.from(m ? m[1] : '');
let owner = null;
const same = t => { const want = Buffer.from(String(t || '')); return want.length && want.length === got.length && crypto.timingSafeEqual(want, got); };
let body = null;
if (m) for (const u of users.all()) {
if (u.disabled) continue;
const want = Buffer.from(String(readJ(path.join(users.dirOf(u.id), FEED_FILE), {}).token || ''));
if (want.length && want.length === got.length && crypto.timingSafeEqual(want, got)) { owner = u; break; }
const dir = dirFor(u.id);
if (same(readJ(path.join(dir, FEED_FILE), {}).token)) { body = icsOf(dir); break; }
const c = calsIn(dir).find(x => same(x.token));
if (c) { body = icsOf(dir, [c.id]); break; }
}
if (!owner) { res.writeHead(404); return res.end('Not found'); }
if (body == null) { res.writeHead(404); return res.end('Not found'); }
res.writeHead(200, { 'content-type': 'text/calendar; charset=utf-8', 'cache-control': 'private, max-age=300' });
res.end(users.runAs(owner, () => localIcs()));
res.end(body);
}
// ---- HTTP API (called by server.js, behind the passkey gate)
const readBody = req => new Promise((ok, no) => { const c = []; req.on('data', d => c.push(d)); req.on('end', () => ok(Buffer.concat(c).toString())); req.on('error', no); });
const json = (res, code, obj) => { res.writeHead(code, { 'content-type': 'application/json' }); res.end(JSON.stringify(obj)); };
const COLORS = ['#4f7cff', '#2f9e6e', '#e0742b', '#d9453a', '#8a5cf0', '#e5a00d'];
/** Every event in the window from all calendars (ICS links, iCloud, NE Hub): { events, errors } */
/** Every event in the window from all calendars (ICS links, iCloud, NE Hub, shared): { events, errors } */
async function collectEvents(win) {
const all = [], errors = [];
const all = [], errors = [], over = colorOverrides();
await Promise.all(load().map(async c => {
try { for (const e of expand(parseEvents(await getIcs(c.url)), win)) all.push({ ...e, cal: c.name, color: c.color }); }
try { for (const e of expand(parseEvents(await getIcs(c.url)), win)) all.push({ ...e, cal: c.name, calId: c.id, color: c.color }); }
catch (e) { errors.push(`${c.name}: ${e.message}`); }
}));
try {
for (const r of await caldav.listRaw(win.from, win.to))
for (const e of expand(parseEvents(r.ics), win)) all.push({ ...e, cal: r.name, calId: r.calId, color: r.color, editable: true, href: r.href, etag: r.etag });
for (const e of expand(parseEvents(r.ics), win)) all.push({ ...e, cal: r.name, calId: r.calId, color: over[r.calId] || r.color, editable: true, href: r.href, etag: r.etag });
} catch (e) { errors.push('iCloud: ' + e.message); }
try { for (const e of expand(parseEvents(localIcs()), win)) all.push({ ...e, cal: LOCAL.name, calId: 'local', color: LOCAL.color, editable: true }); }
catch (e) { errors.push('NE Hub: ' + e.message); }
try {
const mine = dirFor(myId()), evs = eventsIn(mine);
const add = (dir, evs, c, id, extra) => {
const ics_ = ics.buildCalendar(evs.filter(e => evCal(e) === c.id).map(e => ({ uid: e.uid, fields: e.fields })));
for (const e of expand(parseEvents(ics_), win)) all.push({ ...e, cal: c.name, calId: id, color: over[id] || c.color || DEFAULT_COLOR, ...extra });
};
for (const c of calsIn(mine)) add(mine, evs, c, c.id, { editable: true });
for (const sh of sharedWithMe()) add(dirFor(sh.owner), eventsIn(dirFor(sh.owner)), sh.cal, sh.id, { editable: sh.mode === 'edit', shared: true, owner: sh.ownerName });
} catch (e) { errors.push('NE Hub: ' + e.message); }
return { events: all, errors };
}
async function handle(req, res) {
const u = new URL(req.url, 'http://x');
try {
if (u.pathname === '/api/cal/list') return json(res, 200, [{ ...LOCAL, local: true }, ...caldav.enabled().map(c => ({ ...c, icloud: true })), ...load().map(({ id, name, color }) => ({ id, name, color }))]);
if (u.pathname === '/api/cal/list') {
const over = colorOverrides();
return json(res, 200, [
...calsIn(users.dir()).map(c => ({ id: c.id, name: c.name, color: c.color || DEFAULT_COLOR, local: true, shares: (c.shares || []).filter(x => users.get(x.user)) })),
...sharedWithMe().map(s => ({ id: s.id, name: s.cal.name, color: over[s.id] || s.cal.color || DEFAULT_COLOR, local: true, shared: true, owner: s.ownerName, mode: s.mode })),
...caldav.enabled().map(c => ({ ...c, color: over[c.id] || c.color, icloud: true })),
...load().map(({ id, name, color }) => ({ id, name, color }))]);
}
if (u.pathname === '/api/cal/ncal' && req.method === 'POST') { // create
const { name, color } = JSON.parse(await readBody(req));
const cals = calsIn(users.dir()), nm = String(name || '').replace(/\s+/g, ' ').trim().slice(0, 40);
if (!nm) throw new Error('Give the calendar a name');
if (cals.length >= 30) throw new Error('That is plenty of calendars');
const c = { id: crypto.randomBytes(5).toString('hex'), name: nm, color: HEX.test(color) ? color : COLORS[cals.length % COLORS.length], shares: [] };
saveCals([...cals, c]);
return json(res, 200, { ok: true, id: c.id });
}
if (u.pathname === '/api/cal/ncal/update' && req.method === 'POST') { // rename, recolor, share
const b = JSON.parse(await readBody(req));
const cals = calsIn(users.dir()), c = cals.find(x => x.id === b.id);
if (!c) throw new Error('Calendar not found');
if (b.name != null) { const nm = String(b.name).replace(/\s+/g, ' ').trim().slice(0, 40); if (!nm) throw new Error('Give the calendar a name'); c.name = nm; }
if (b.color != null) { if (!HEX.test(b.color)) throw new Error('Pick a color'); c.color = b.color; }
if (b.shares != null) {
if (!Array.isArray(b.shares)) throw new Error('Bad share list');
const seen = new Set();
c.shares = b.shares.filter(x => x && x.user !== myId() && users.get(x.user) && !seen.has(x.user) && seen.add(x.user)).map(x => ({ user: x.user, mode: x.mode === 'edit' ? 'edit' : 'view' }));
}
saveCals(cals);
return json(res, 200, { ok: true });
}
if (u.pathname === '/api/cal/ncal/delete' && req.method === 'POST') {
const { id } = JSON.parse(await readBody(req));
const cals = calsIn(users.dir());
if (!cals.some(x => x.id === id)) throw new Error('Calendar not found');
if (cals.length < 2) throw new Error('Keep at least one NE Hub calendar');
saveCals(cals.filter(x => x.id !== id));
saveEvents(users.dir(), eventsIn(users.dir()).filter(e => evCal(e) !== id));
return json(res, 200, { ok: true });
}
if (u.pathname === '/api/cal/ncal/feed' && req.method === 'POST') { // one calendar's subscribe link (reset: true makes a new one)
const b = JSON.parse(await readBody(req));
const origin = (process.env.PUBLIC_URL || '').replace(/\/$/, '') || `${req.headers['x-forwarded-proto'] || 'http'}://${req.headers['x-forwarded-host'] || req.headers.host}`;
return json(res, 200, { url: `${origin}/cal/feed/${calToken(b.id, !!b.reset)}.ics` });
}
if (u.pathname === '/api/cal/people') // who a calendar can be shared with
return json(res, 200, users.all().filter(x => !x.disabled && x.id !== myId()).map(x => ({ id: x.id, name: x.name })));
if (u.pathname === '/api/cal/color' && req.method === 'POST') { // your own color for iCloud, shared or link calendars
const { id, color } = JSON.parse(await readBody(req));
if (!HEX.test(color)) throw new Error('Pick a color');
const link = load(), l = link.find(x => x.id === id);
if (l) { l.color = color; save(link); } else writeJ(COLOR_FILE, { ...colorOverrides(), [id]: color });
return json(res, 200, { ok: true });
}
if (u.pathname === '/api/cal/add' && req.method === 'POST') {
const { name, url } = JSON.parse(await readBody(req));
if (!url) throw new Error('Paste the calendar link');
@@ -298,10 +411,11 @@ async function handle(req, res) {
}
if (u.pathname === '/api/cal/event' && req.method === 'GET') {
const q = u.searchParams, tz = q.get('tz') || 'UTC';
if (q.get('cal') === 'local') {
const e = loadLocal().find(x => x.uid === q.get('uid'));
const r = !caldav.isCal(q.get('cal')) && resolveCal(q.get('cal'));
if (r) {
const e = eventsIn(r.dir).find(x => x.uid === q.get('uid') && evCal(x) === r.cid);
if (!e) throw new Error('Event not found');
return json(res, 200, { cal: 'local', uid: e.uid, fields: e.fields });
return json(res, 200, { cal: q.get('cal'), uid: e.uid, fields: e.fields });
}
if (caldav.isCal(q.get('cal'))) {
const r = await caldav.getResource(q.get('cal'), q.get('href'));
@@ -313,12 +427,14 @@ async function handle(req, res) {
}
if (u.pathname === '/api/cal/event' && req.method === 'POST') {
const b = JSON.parse(await readBody(req));
if (b.cal === 'local') {
const r = !caldav.isCal(b.cal) && resolveCal(b.cal);
if (r) {
if (!canWrite(r)) throw new Error('This calendar is shared with you to view only');
ics.validate(b.fields);
const all = loadLocal(), uid = b.uid || ics.newUid(), i = all.findIndex(x => x.uid === uid);
const rec = { uid, fields: b.fields, updated: Date.now() };
const all = eventsIn(r.dir), uid = b.uid || ics.newUid(), i = all.findIndex(x => x.uid === uid && evCal(x) === r.cid);
const rec = { uid, cal: r.cid, fields: b.fields, updated: Date.now() };
if (i >= 0) all[i] = { ...all[i], ...rec }; else all.push({ ...rec, created: Date.now() });
writeJ(EV_FILE, all);
saveEvents(r.dir, all);
return json(res, 200, { ok: true, uid });
}
if (caldav.isCal(b.cal)) {
@@ -336,7 +452,12 @@ async function handle(req, res) {
}
if (u.pathname === '/api/cal/event/delete' && req.method === 'POST') {
const b = JSON.parse(await readBody(req));
if (b.cal === 'local') { writeJ(EV_FILE, loadLocal().filter(x => x.uid !== b.uid)); return json(res, 200, { ok: true }); }
const r = !caldav.isCal(b.cal) && resolveCal(b.cal);
if (r) {
if (!canWrite(r)) throw new Error('This calendar is shared with you to view only');
saveEvents(r.dir, eventsIn(r.dir).filter(x => !(x.uid === b.uid && evCal(x) === r.cid)));
return json(res, 200, { ok: true });
}
if (caldav.isCal(b.cal)) { await caldav.deleteEvent(b.cal, b.href, b.etag); return json(res, 200, { ok: true }); }
throw new Error('Unknown calendar');
}