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 = ?), ?) on conflict(reader_url) do nothing `, 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(readerUrlOrBookId: string | 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.reader_url = ? or books.id = ?`, readerUrlOrBookId, Number(readerUrlOrBookId), ); 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}`); } if (!book.title) { book.title = String(book.id); } if (book.authors.length > 0) { book.title = `[${book.authors .slice(0, opts.outAuthorsLimit) .join(", ")}] ${book.title}`.replace(/[/]/g, "%2F"); } await fs.mkdir(opts.outDir, { recursive: true, }); const files = await fs.readdir(path); if (files.every((f) => f.match(/[.](zip|cbz)$/))) { const digits = String(files.length).length; function pad(n: string) { return n.padStart(digits, "0"); } for (const [n, f] of Object.entries(files)) { await fs.copyFile( `${path}/${f}`, `${opts.outDir}/${book.title}${ files.length > 1 ? ` - ${pad(n)}` : "" }.${f.split(".").at(-1)}`, ); } await fs.rm(path, { recursive: true }); return; } const out = createWriteStream(`${opts.outDir}/${book.title}.cbz`); const zip = new Zip(function cb(err, data, final) { if (err) { out.destroy(err); return; } out[final ? "end" : "write"](data); }); 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 }); }, }; }