35 lines
3.5 KiB
JavaScript
35 lines
3.5 KiB
JavaScript
// Where each person has got to in each thing they listen to (audiobooks, podcast episodes), kept on the server so every device agrees.
|
|
// One file per person; for each item the newest report wins, so an old device cannot overwrite a newer position.
|
|
const fs = require('fs'), path = require('path');
|
|
const users = require('./users');
|
|
|
|
// With a `kind` ('podcast' or 'audiobook'), each report also adds the time actually listened to that person's listening log (listening.json): seconds per day per kind, and
|
|
// the items finished. A jump forward bigger than the time that passed (a seek) does not count.
|
|
const day = t => { const d = new Date(t); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; };
|
|
const logFile = () => path.join(users.dir(), 'listening.json');
|
|
const loadLog = () => { try { const o = JSON.parse(fs.readFileSync(logFile(), 'utf8')); return { days: o.days || {}, finished: o.finished || {} }; } catch { return { days: {}, finished: {} }; } };
|
|
const stats = () => {
|
|
const o = loadLog(), now = new Date(), today = day(now), tot = k => Object.values(o.days).reduce((n, d) => n + (d[k] || 0), 0), sum = (a, b) => Object.entries(o.days).filter(([k]) => k >= a && k <= b).reduce((n, [, d]) => n + (d.podcast || 0) + (d.audiobook || 0), 0);
|
|
const back = n => { const d = new Date(now); d.setDate(d.getDate() - n); return day(d); }, any = k => ((o.days[k] || {}).podcast || 0) + ((o.days[k] || {}).audiobook || 0) >= 60;
|
|
let streak = 0; for (let i = any(today) ? 0 : 1; any(back(i)); i++) streak++;
|
|
const year = String(now.getFullYear());
|
|
return { today: sum(today, today), week: sum(back(6), today), month: sum(back(29), today), podcast: tot('podcast'), audiobook: tot('audiobook'), streak, last14: [...Array(14)].map((_, i) => { const k = back(13 - i); return { day: k, s: ((o.days[k] || {}).podcast || 0) + ((o.days[k] || {}).audiobook || 0) }; }), finishedYear: Object.values(o.finished).filter(f => f.kind === 'audiobook' && day(f.t).startsWith(year)).length, finishedAll: Object.keys(o.finished).length };
|
|
};
|
|
module.exports = (name, kind) => {
|
|
const file = () => path.join(users.dir(), name);
|
|
const load = () => { try { return JSON.parse(fs.readFileSync(file(), 'utf8')); } catch { return {}; } };
|
|
const merge = body => {
|
|
const mine = load(), log = kind ? loadLog() : null; let logged = false;
|
|
for (const [k, v] of Object.entries(body || {})) if (/^[\w.:-]{1,80}$/.test(k) && v && typeof v === 'object' && (v.t || 0) >= ((mine[k] || {}).t || 0)) {
|
|
const old = mine[k], n = { pos: +v.pos || 0, dur: +v.dur || 0, t: +v.t || 0 };
|
|
if (log && old && n.t > old.t) { const wall = (n.t - old.t) / 1000, gain = n.pos - old.pos; if (gain > 0 && gain <= wall * 2 + 5 && wall < 600) { const d = log.days[day(n.t)] = log.days[day(n.t)] || {}; d[kind] = Math.round(((d[kind] || 0) + Math.min(gain, wall + 5)) * 10) / 10; logged = true; } }
|
|
if (log && n.dur > 60 && n.pos >= n.dur - 30 && !log.finished[k]) { log.finished[k] = { t: n.t || Date.now(), kind }; logged = true; }
|
|
mine[k] = n;
|
|
}
|
|
if (logged) { for (const k of Object.keys(log.days).sort().slice(0, -400)) delete log.days[k]; const lf = logFile(), lt = lf + '.tmp'; fs.writeFileSync(lt, JSON.stringify(log), { mode: 0o600 }); fs.renameSync(lt, lf); }
|
|
const f = file(), tmp = f + '.tmp'; fs.writeFileSync(tmp, JSON.stringify(mine), { mode: 0o600 }); fs.renameSync(tmp, f); return mine;
|
|
};
|
|
return { load, merge };
|
|
};
|
|
module.exports.stats = stats;
|