159 lines
3.5 KiB
JavaScript
159 lines
3.5 KiB
JavaScript
/**
|
|
* 纯 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,
|
|
};
|