My Store: Add many books, with title, author, description and cover read out of each EPUB
build-container / build (push) Successful in 3m32s
build-container / build (push) Successful in 3m32s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
272760c321
commit
3376499805
5 files changed
+98
-1
No files matched your search
+17
@@ -7,11 +7,13 @@ const { pipeline } = require('stream/promises');
|
||||
const atomicWrite = require('./atomic');
|
||||
const users = require('./users'), settings = require('./settings'), books = require('./books');
|
||||
const { readBody } = require('./body');
|
||||
const { readEpub } = require('./epubmeta');
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data');
|
||||
const ROOT = () => path.join(DATA_DIR, 'mystore'), ITEMS = () => path.join(ROOT(), 'items.json'), FILES = () => path.join(ROOT(), 'files');
|
||||
const SEAFILE = () => (process.env.SEAFILE_URL || 'https://seafile.necloud.us').replace(/\/$/, '');
|
||||
const MAX_FILE = 1024 * 1024 * 1024, MAX_COVER = 3 * 1024 * 1024, MAX_EXT = 2 * 1024 * 1024, MAX_STATE = 256 * 1024, MAX_FILES_PER_ITEM = 200;
|
||||
const MAX_BOOK_IMPORT = 300 * 1024 * 1024;
|
||||
const KINDS = ['book', 'music', 'app', 'extension', 'link'];
|
||||
const PERMS = ['tasks.read', 'tasks.write', 'contacts.read', 'contacts.write', 'calendar.read', 'calendar.write', 'notes.read', 'notes.write', 'recipes.read', 'music.read']; // what an extension may ask the person for
|
||||
const BOOK_EXT = ['epub', 'pdf', 'mobi', 'azw3', 'azw', 'docx'];
|
||||
@@ -107,6 +109,21 @@ async function handle(req, res) {
|
||||
if (it.kind === 'link') it.embed = !!b.embed;
|
||||
if (!old) { if (l.length >= 2000) throw fail(400, 'That is plenty of items'); l.push(it); } saveItems(l); return json(res, 200, view(it));
|
||||
}
|
||||
if (p === '/import' && req.method === 'POST') { // bulk add: the body is one book file (its name is in ?name=); the title, author, description and cover come out of the book itself (admin)
|
||||
needAdmin(me); const name = safeName(u.searchParams.get('name')), ext = extOf(name);
|
||||
if (!BOOK_EXT.includes(ext)) throw fail(400, 'A book must be an EPUB, PDF, MOBI, AZW3 or DOCX file');
|
||||
const l = loadItems(); if (l.length >= 2000) throw fail(400, 'That is plenty of items');
|
||||
const it = { id: newId(), kind: 'book', files: [], cover: false, added: Date.now() }, fid = newId(), file = fileOn(it, fid);
|
||||
const size = await receive(req, file, MAX_BOOK_IMPORT), buf = fs.readFileSync(file), hash = crypto.createHash('sha1').update(buf).digest('hex');
|
||||
const dup = l.find(x => x.kind === 'book' && x.files.some(f => f.hash === hash));
|
||||
if (dup) { fs.rmSync(file, { force: true }); return json(res, 200, { duplicate: true, item: view(dup) }); }
|
||||
let meta = {}; if (ext === 'epub') { try { meta = readEpub(buf); } catch { meta = {}; } }
|
||||
const plain = path.basename(name, '.' + ext).replace(/_+/g, ' ').trim();
|
||||
Object.assign(it, { title: str(meta.title, 120) || str(plain, 120) || 'Untitled', creator: str(meta.author, 120), description: str(meta.description, 2000), version: '', url: '', updated: Date.now() });
|
||||
it.files.push({ id: fid, name, size, ext, hash });
|
||||
if (meta.cover && meta.cover.data.length <= MAX_COVER) { fs.writeFileSync(coverOn(it), meta.cover.data, { mode: 0o600 }); it.cover = meta.cover.type; }
|
||||
l.push(it); saveItems(l); return json(res, 200, { item: view(it), noAuthor: !it.creator, noCover: !it.cover });
|
||||
}
|
||||
if ((m = /^\/items\/([0-9a-f]{12})$/.exec(p)) && req.method === 'DELETE') {
|
||||
needAdmin(me); const l = loadItems(), it = l.find(x => x.id === m[1]); if (!it) throw fail(404, 'Item not found');
|
||||
for (const f of it.files) fs.rmSync(fileOn(it, f.id), { force: true }); fs.rmSync(coverOn(it), { force: true }); saveItems(l.filter(x => x !== it)); return json(res, 200, { ok: true });
|
||||
|
||||
Reference in new issue
Block a user