gadl/library.ts

160 lines
4 KiB
TypeScript
Raw Normal View History

2023-11-20 01:37:55 +09:00
import fs from "node:fs/promises";
import { createWriteStream } from "node:fs";
import stream from "node:stream/promises";
import { Zip, ZipPassThrough } from "fflate";
2023-11-19 02:25:32 +09:00
import { Database } from "./database";
2023-12-02 23:22:33 +09:00
import { type TPlatform, site } from "./platform";
2023-11-19 02:25:32 +09:00
export type Book = {
id: number;
2023-12-02 23:22:33 +09:00
platform: TPlatform;
2023-11-19 02:25:32 +09:00
readerUrl: string;
2023-11-20 22:56:09 +09:00
title: string;
authors: Array<string>;
2023-11-19 02:25:32 +09:00
};
export function createLibrary(db: Database) {
return {
2023-11-20 22:56:09 +09:00
async add(readerUrlOrBook: string | Book) {
if (typeof readerUrlOrBook === "string") {
2023-12-02 23:22:33 +09:00
const platform = site(readerUrlOrBook);
2023-11-20 22:56:09 +09:00
await db.run(
2023-12-03 05:54:45 +09:00
`\
insert into books(
platform_id,
reader_url)
values((select id from platforms where name = ?), ?)
on conflict(reader_url)
do nothing
`,
2023-11-20 22:56:09 +09:00
platform,
readerUrlOrBook,
);
return;
}
2023-11-19 02:25:32 +09:00
await db.run(
2023-11-20 22:56:09 +09:00
`\
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
`,
2023-12-02 23:22:33 +09:00
readerUrlOrBook.platform,
2023-11-20 22:56:09 +09:00
readerUrlOrBook.readerUrl,
readerUrlOrBook.title,
JSON.stringify(readerUrlOrBook.authors),
2023-11-19 02:25:32 +09:00
);
2023-11-19 20:51:29 +09:00
},
async delete(id: number) {
await db.run(`delete from books where id = ?`, id);
2023-11-19 02:25:32 +09:00
},
2023-12-03 05:54:45 +09:00
async get(readerUrlOrBookId: string | number): Promise<Book | undefined> {
2023-11-20 22:56:09 +09:00
const row = await db.get(
2023-12-03 05:54:45 +09:00
`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),
2023-11-19 22:04:19 +09:00
);
2023-11-20 22:56:09 +09:00
const book: Book | undefined = row && {
...row,
authors: JSON.parse(row.authors),
};
2023-11-19 22:04:19 +09:00
return book;
},
2023-11-19 02:25:32 +09:00
async getBooks(): Promise<Array<Book>> {
2023-11-20 22:56:09 +09:00
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`,
2023-11-19 02:25:32 +09:00
);
2023-11-20 22:56:09 +09:00
const books: Array<Book> = rows.map((row) => ({
...row,
authors: JSON.parse(row.authors),
}));
2023-11-19 02:25:32 +09:00
return books;
},
async archive(
path: string,
book: Book,
opts: {
outDir: string;
outAuthorsLimit: number;
},
) {
2023-11-23 10:24:08 +09:00
const bookDir = await fs.stat(path);
2023-11-20 01:37:55 +09:00
2023-11-23 10:24:08 +09:00
if (!bookDir.isDirectory()) {
throw new Error(`Not found: ${path}`);
}
2023-12-03 05:54:45 +09:00
if (!book.title) {
book.title = String(book.id);
}
if (book.authors.length > 0) {
2023-12-24 23:15:06 +09:00
book.title = `[${book.authors
.slice(0, opts.outAuthorsLimit)
.join(", ")}] ${book.title}`.replace(/[/]/g, "%2F");
2023-12-03 05:54:45 +09:00
}
2023-12-24 23:09:32 +09:00
await fs.mkdir(opts.outDir, {
recursive: true,
});
2023-12-03 12:44:31 +09:00
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)) {
2023-12-24 23:09:32 +09:00
await fs.copyFile(
2023-12-03 12:44:31 +09:00
`${path}/${f}`,
2023-12-24 23:15:06 +09:00
`${opts.outDir}/${book.title}${
2023-12-03 12:44:31 +09:00
files.length > 1 ? ` - ${pad(n)}` : ""
}.${f.split(".").at(-1)}`,
);
}
2023-12-24 23:09:32 +09:00
await fs.rm(path, { recursive: true });
2023-12-03 12:44:31 +09:00
return;
}
2023-12-24 23:15:06 +09:00
const out = createWriteStream(`${opts.outDir}/${book.title}.cbz`);
2023-11-20 01:37:55 +09:00
2023-11-23 10:24:08 +09:00
const zip = new Zip(function cb(err, data, final) {
if (err) {
out.destroy(err);
return;
2023-11-20 01:37:55 +09:00
}
2023-11-23 10:24:08 +09:00
out[final ? "end" : "write"](data);
});
2023-11-20 01:37:55 +09:00
2023-11-23 10:24:08 +09:00
for (const file of files) {
const data = new ZipPassThrough(file);
zip.add(data);
const buffer = await fs.readFile(`${path}/${file}`);
data.push(buffer, true);
2023-11-20 01:37:55 +09:00
}
2023-11-23 10:24:08 +09:00
zip.end();
await stream.finished(out);
await fs.rm(path, { recursive: true });
2023-11-20 01:37:55 +09:00
},
2023-11-19 02:25:32 +09:00
};
}