feat(api,web): support CBR — extraction RAR, scan et lecture d'albums
- api: cbr utilitaire (extraction RAR), scanner/métadonnées (+ tests), books, schéma - web: ReaderPage/locators (+ tests), mockData, types partagés - deps: pnpm-lock Refs: #17
This commit is contained in:
@ -28,12 +28,12 @@ export class BooksController {
|
||||
|
||||
@Get(":id/pages")
|
||||
pages(@Param("id") id: string) {
|
||||
return this.books.listCbzPages(Number(id));
|
||||
return this.books.listComicPages(Number(id));
|
||||
}
|
||||
|
||||
@Get(":id/pages/:page")
|
||||
page(@Param("id") id: string, @Param("page") page: string, @Res() reply: FastifyReply) {
|
||||
const result = this.books.readCbzPage(Number(id), Number(page));
|
||||
async page(@Param("id") id: string, @Param("page") page: string, @Res() reply: FastifyReply) {
|
||||
const result = await this.books.readComicPage(Number(id), Number(page));
|
||||
reply.header("Content-Type", result.contentType);
|
||||
reply.header("Cache-Control", "private, max-age=3600");
|
||||
return reply.send(result.data);
|
||||
|
||||
@ -3,6 +3,7 @@ import { createReadStream, existsSync } from "node:fs";
|
||||
import { extname } from "node:path";
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import { BookQueryDto } from "@readabook/shared";
|
||||
import { listCbrImageEntries, readCbrPage } from "../common/cbr.js";
|
||||
import { listCbzImageEntries, readCbzPage } from "../common/cbz.js";
|
||||
import { DatabaseService } from "../database/database.service.js";
|
||||
import { books } from "../database/schema.js";
|
||||
@ -68,10 +69,10 @@ export class BooksService {
|
||||
return { book, stream: createReadStream(book.coverPath), coverPath: book.coverPath };
|
||||
}
|
||||
|
||||
listCbzPages(id: number) {
|
||||
async listComicPages(id: number) {
|
||||
const book = this.get(id);
|
||||
this.assertCbzBook(book);
|
||||
const pages = listCbzImageEntries(book.filePath);
|
||||
this.assertComicArchiveBook(book);
|
||||
const pages = book.format === "cbr" ? await listCbrImageEntries(book.filePath) : listCbzImageEntries(book.filePath);
|
||||
return {
|
||||
bookId: book.id,
|
||||
pageCount: pages.length,
|
||||
@ -79,14 +80,17 @@ export class BooksService {
|
||||
};
|
||||
}
|
||||
|
||||
readCbzPage(id: number, page: number) {
|
||||
async readComicPage(id: number, page: number) {
|
||||
const book = this.get(id);
|
||||
this.assertCbzBook(book);
|
||||
this.assertComicArchiveBook(book);
|
||||
try {
|
||||
const result = readCbzPage(book.filePath, page);
|
||||
const result =
|
||||
book.format === "cbr"
|
||||
? await readCbrPage(book.filePath, page, this.database.config.storageDir)
|
||||
: readCbzPage(book.filePath, page);
|
||||
return { book, page, contentType: lookupMime(result.entryName), data: result.data };
|
||||
} catch (error) {
|
||||
throw new NotFoundException(error instanceof Error ? error.message : "CBZ page not found");
|
||||
throw new NotFoundException(error instanceof Error ? error.message : "Comic page not found");
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,9 +98,9 @@ export class BooksService {
|
||||
return this.database.db.select({ count: sql<number>`count(*)` }).from(books).get()?.count ?? 0;
|
||||
}
|
||||
|
||||
private assertCbzBook(book: typeof books.$inferSelect): void {
|
||||
if (book.format !== "cbz") {
|
||||
throw new BadRequestException("Book is not a CBZ archive");
|
||||
private assertComicArchiveBook(book: typeof books.$inferSelect): void {
|
||||
if (book.format !== "cbz" && book.format !== "cbr") {
|
||||
throw new BadRequestException("Book is not a comic archive");
|
||||
}
|
||||
if (!existsSync(book.filePath)) {
|
||||
throw new NotFoundException("Book file not found on disk");
|
||||
|
||||
Reference in New Issue
Block a user