156 lines
4 KiB
TypeScript
156 lines
4 KiB
TypeScript
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<string>;
|
|
};
|
|
|
|
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<Book | undefined> {
|
|
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<Array<Book>> {
|
|
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<Book> = 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.title}」`;
|
|
}
|
|
|
|
const title = `${book.authors.slice(0, opts.outAuthorsLimit).join("、")}${
|
|
book.title
|
|
}`.replace(/[/]/g, "%2F");
|
|
|
|
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.rename(
|
|
`${path}/${f}`,
|
|
`${opts.outDir}/${title}${
|
|
files.length > 1 ? ` - ${pad(n)}` : ""
|
|
}.${f.split(".").at(-1)}`,
|
|
);
|
|
}
|
|
await fs.rmdir(path);
|
|
return;
|
|
}
|
|
|
|
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);
|
|
});
|
|
|
|
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 });
|
|
},
|
|
};
|
|
}
|