/** * 纯 JSON 文件存储,替代 SQLite。无 node-gyp / 原生依赖,任意环境可运行。 */ const fs = require('fs'); const path = require('path'); const DATA_DIR = path.join(__dirname, 'data'); const STORE_PATH = path.join(DATA_DIR, 'store.json'); const DEFAULT_STORE = { stats: { view_count: 0, like_count: 0, share_count: 0, watching_now: 0, }, viewers: {}, comments: [], settings: { danmaku_enabled: '0', danmaku_position: 'top', }, }; let _store = null; function load() { if (_store) return _store; if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true }); if (fs.existsSync(STORE_PATH)) { try { _store = JSON.parse(fs.readFileSync(STORE_PATH, 'utf8')); if (!_store.stats) _store.stats = DEFAULT_STORE.stats; if (!_store.viewers) _store.viewers = {}; if (!Array.isArray(_store.comments)) _store.comments = []; if (!_store.settings) _store.settings = DEFAULT_STORE.settings; return _store; } catch (e) {} } _store = JSON.parse(JSON.stringify(DEFAULT_STORE)); save(); return _store; } function save() { if (!_store) return; fs.writeFileSync(STORE_PATH, JSON.stringify(_store, null, 0), 'utf8'); } function initDb() { load(); } function getConfig() { const s = load().settings; return { danmakuEnabled: s.danmaku_enabled === '1', danmakuPosition: (s.danmaku_position || 'top').toLowerCase(), }; } function getStats() { const st = load().stats; const commentCount = load().comments.length; return { viewCount: st.view_count, commentCount, likeCount: st.like_count, shareCount: st.share_count, watchingNow: st.watching_now, }; } function incView() { const s = load(); s.stats.view_count += 1; save(); return getStats(); } function incLike() { const s = load(); s.stats.like_count += 1; save(); return { likeCount: s.stats.like_count }; } function incShare() { const s = load(); s.stats.share_count += 1; save(); return { shareCount: s.stats.share_count }; } function joinViewer(viewerId) { const s = load(); const now = Date.now(); s.viewers[viewerId] = now; const cutoff = now - 120000; Object.keys(s.viewers).forEach((id) => { if (s.viewers[id] < cutoff) delete s.viewers[id]; }); s.stats.watching_now = Object.keys(s.viewers).length; save(); return s.stats.watching_now; } function leaveViewer(viewerId) { const s = load(); delete s.viewers[viewerId]; s.stats.watching_now = Object.keys(s.viewers).length; save(); return s.stats.watching_now; } function getComments(limit = 100) { const list = load().comments; const slice = list.slice(-Math.min(limit, list.length)); return slice.map((r) => ({ id: r.id, content: r.content, nickname: r.nickname || '游客', createdAt: r.created_at, })); } function addComment(content, nickname) { const s = load(); const now = Date.now(); const id = s.comments.length ? Math.max(...s.comments.map((c) => c.id)) + 1 : 1; const row = { id, content: String(content).trim().slice(0, 200) || '(空)', nickname: nickname ? String(nickname).trim().slice(0, 32) : null, created_at: now, }; s.comments.push(row); save(); return { id: row.id, content: row.content, nickname: row.nickname || '游客', createdAt: row.created_at, }; } module.exports = { initDb, getConfig, getStats, incView, incLike, incShare, joinViewer, leaveViewer, getComments, addComment, };