import fs from "node:fs/promises"; import { createWriteStream } from "node:fs"; import stream from "node:stream/promises"; import { Zip, ZipPassThrough } from "fflate"; import { Database } from "./database"; import { type TPlatform, site } from "./platform"; export type Book = { id: number; platform: TPlatform; readerUrl: string; title: string; authors: Array; }; export function createLibrary(db: Database) { return { async add(readerUrlOrBook: string | Book) { if (typeof readerUrlOrBook === "string") { const platform = site(readerUrlOrBook); await db.run( `insert into books(platform_id, reader_url) values((select id from platforms where name = ?), ?)`, platform, readerUrlOrBook, ); return; } await db.run( `\ insert into books( platform_id, reader_url, title, authors) values((select id from platforms where name = ?), ?, ?, ?) on conflict(reader_url) do update set title = excluded.title, authors = excluded.authors `, readerUrlOrBook.platform, readerUrlOrBook.readerUrl, readerUrlOrBook.title, JSON.stringify(readerUrlOrBook.authors), ); }, async delete(id: number) { await db.run(`delete from books where id = ?`, id); }, async get(id: number): Promise { const row = await db.get( `select books.id, platforms.name as platform, books.reader_url as readerUrl, books.title, books.authors from books left join platforms on books.platform_id = platforms.id where books.id = ?`, id, ); const book: Book | undefined = row && { ...row, authors: JSON.parse(row.authors), }; return book; }, async getBooks(): Promise> { const rows = await db.all( `select books.id, platforms.name as platform, books.reader_url as readerUrl, books.title, books.authors from books left join platforms on books.platform_id = platforms.id`, ); const books: Array = rows.map((row) => ({ ...row, authors: JSON.parse(row.authors), })); return books; }, async archive( path: string, book: Book, opts: { outDir: string; outAuthorsLimit: number; }, ) { const bookDir = await fs.stat(path); if (!bookDir.isDirectory()) { throw new Error(`Not found: ${path}`); } const title = `${book.authors .slice(0, opts.outAuthorsLimit) .join("、")}「${book.title}」`.replace(/[/]/g, "%2F"); const out = createWriteStream(`${opts.outDir}/${title}.cbz`); const zip = new Zip(function cb(err, data, final) { if (err) { out.destroy(err); return; } out[final ? "end" : "write"](data); }); const files = await fs.readdir(path); for (const file of files) { const data = new ZipPassThrough(file); zip.add(data); const buffer = await fs.readFile(`${path}/${file}`); data.push(buffer, true); } zip.end(); await stream.finished(out); await fs.rm(path, { recursive: true }); }, }; }