Books: book store with Project Gutenberg, Standard Ebooks, Internet Archive and OPDS catalogues

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaC9HmtHMdRP8dAbN4wAKj
This commit is contained in:
Claude committed 2026-10-25 01:07:39 +00:00
1 parent ca47dc46e4
commit 84ba0000b8
8 files changed
+341 -12

No files matched your search

+18 -9
View File
@@ -48,23 +48,32 @@ function mergeNotes(have, sent) {
return [...by.values()].filter(n => !n.del || Date.now() - n.t < 30 * 864e5);
}
/** Put a book on the signed-in person's shelf (the same checks for an upload and for a download from the store). Returns the book; `duplicate` is set when it was already there. */
function addBook(buf, ext, meta = {}, cover) {
if (!TYPES[ext]) throw new Error('Only EPUB, PDF, DOCX, MOBI, AZW and AZW3 books can be added');
if (buf.length > MAX_BOOK) throw new Error('That file is larger than 300 MB');
if (!looksLike(ext, buf)) throw new Error('That does not look like a real ' + ext.toUpperCase() + ' file');
if (MOBI.has(ext) && mobiLocked(buf)) throw new Error('This book has DRM (copy protection), which NE Hub cannot open. Only DRM-free books work.');
const hash = crypto.createHash('sha1').update(buf).digest('hex'), list = index(), dup = list.find(b => b.hash === hash);
if (dup) return { ...dup, duplicate: true };
const id = crypto.randomBytes(8).toString('hex');
fs.writeFileSync(path.join(dir(), id + '.' + ext), buf, { mode: 0o600 });
const book = { id, ext, hash, size: buf.length, added: Date.now(), title: clip(meta.title, 200) || 'Untitled', author: clip(meta.author, 200), cover: false };
if (cover && COVERS[cover.type] && cover.data.length <= MAX_COVER) { fs.writeFileSync(path.join(dir(), `${id}.cover.${COVERS[cover.type]}`), cover.data, { mode: 0o600 }); book.cover = true; }
list.push(book); saveIndex(list); return book;
}
async function handle(req, res) {
const u = new URL(req.url, 'http://x'), p = u.pathname.slice('/api/read'.length) || '/';
let m;
if (p.startsWith('/store/')) return require('./store').handle(req, res, p.slice('/store'.length));
if (p === '/list' && req.method === 'GET') { const st = state(); return json(res, 200, index().map(b => withState(b, st))); }
if (p === '/upload' && req.method === 'POST') {
const name = clip(u.searchParams.get('name'), 200), ext = path.extname(name).slice(1).toLowerCase();
if (!TYPES[ext]) return json(res, 400, { error: 'Only EPUB, PDF, DOCX, MOBI, AZW and AZW3 books can be added' });
const buf = await body(req, MAX_BOOK); if (!buf) return json(res, 413, { error: 'That file is larger than 300 MB' });
if (!looksLike(ext, buf)) return json(res, 400, { error: 'That does not look like a real ' + ext.toUpperCase() + ' file' });
if (MOBI.has(ext) && mobiLocked(buf)) return json(res, 400, { error: 'This book has DRM (copy protection), which NE Hub cannot open. Only DRM-free books work.' });
const hash = crypto.createHash('sha1').update(buf).digest('hex'), list = index(), dup = list.find(b => b.hash === hash);
if (dup) return json(res, 200, { ...dup, duplicate: true });
const id = crypto.randomBytes(8).toString('hex');
fs.writeFileSync(path.join(dir(), id + '.' + ext), buf, { mode: 0o600 });
const book = { id, ext, hash, size: buf.length, added: Date.now(), title: clip(path.basename(name, '.' + ext).replace(/[_]+/g, ' '), 200) || 'Untitled', author: '', cover: false };
list.push(book); saveIndex(list); return json(res, 200, book);
try { return json(res, 200, addBook(buf, ext, { title: path.basename(name, '.' + ext).replace(/[_]+/g, ' ') })); } catch (e) { return json(res, 400, { error: e.message }); }
}
if (p === '/state' && req.method === 'GET') return json(res, 200, state());
@@ -135,4 +144,4 @@ async function handle(req, res) {
return json(res, 404, { error: 'Not found' });
}
module.exports = { handle, mergeNotes, looksLike };
module.exports = { handle, mergeNotes, looksLike, addBook, index, TYPES };