fix(web,api): lecteur PDF — options pdf.js, assets cmaps/fonts et recherche sans limite par défaut
This commit is contained in:
@ -1,13 +1,15 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { mkdtempSync, readdirSync, rmSync, statSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { extname, join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { BookQuerySchema } from "@readabook/shared";
|
||||
import { DatabaseService } from "../database/database.service.js";
|
||||
import { books, libraries, series } from "../database/schema.js";
|
||||
import { BooksService } from "./books.service.js";
|
||||
|
||||
const previousDatabasePath = process.env.DATABASE_PATH;
|
||||
const previousStorageDir = process.env.STORAGE_DIR;
|
||||
const realBooksPath = "/home/anthony/Documents/Projects/ReadaBook/Books";
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
@ -19,6 +21,59 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("BooksService", () => {
|
||||
it.runIf(canLoadBetterSqlite() && canReadRealBooksCorpus())("exposes every persisted real corpus book through the default catalogue query", () => {
|
||||
const database = createDatabase();
|
||||
try {
|
||||
const service = new BooksService(database);
|
||||
const now = database.now();
|
||||
const library = database.db
|
||||
.insert(libraries)
|
||||
.values({ name: "Real corpus", path: realBooksPath, enabled: true, createdAt: now, updatedAt: now })
|
||||
.returning()
|
||||
.get();
|
||||
const files = realCorpusBookFiles();
|
||||
|
||||
for (const [index, filePath] of files.entries()) {
|
||||
database.db
|
||||
.insert(books)
|
||||
.values({
|
||||
libraryId: library.id,
|
||||
seriesId: null,
|
||||
title: `Corpus ${String(index + 1).padStart(3, "0")}`,
|
||||
author: null,
|
||||
description: null,
|
||||
isbn: null,
|
||||
isbn13: null,
|
||||
identifiersJson: null,
|
||||
localMetadataJson: null,
|
||||
language: null,
|
||||
publisher: null,
|
||||
publishedDate: null,
|
||||
volumeNumber: null,
|
||||
volumeLabel: null,
|
||||
format: bookFormatFromPath(filePath),
|
||||
filePath,
|
||||
coverPath: null,
|
||||
metadataStatus: "none",
|
||||
metadataProvenanceJson: JSON.stringify({ title: "local" }),
|
||||
scanStatus: "succeeded",
|
||||
enrichmentStatus: "idle",
|
||||
fileSize: statSync(filePath).size,
|
||||
fileMtime: statSync(filePath).mtime.toISOString(),
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
expect(files.length).toBeGreaterThan(50);
|
||||
expect(service.count()).toBe(files.length);
|
||||
expect(service.list(BookQuerySchema.parse({}))).toHaveLength(files.length);
|
||||
} finally {
|
||||
database.onModuleDestroy();
|
||||
}
|
||||
});
|
||||
|
||||
it.runIf(canLoadBetterSqlite())("exposes metadata status and parsed provenance on book API rows", () => {
|
||||
const database = createDatabase();
|
||||
const service = new BooksService(database);
|
||||
@ -178,3 +233,32 @@ function canLoadBetterSqlite(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function canReadRealBooksCorpus(): boolean {
|
||||
try {
|
||||
return realCorpusBookFiles().length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function realCorpusBookFiles(root = realBooksPath): string[] {
|
||||
return readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
|
||||
const path = join(root, entry.name);
|
||||
if (entry.isDirectory()) return realCorpusBookFiles(path);
|
||||
if (!entry.isFile()) return [];
|
||||
return isBookFile(path) ? [path] : [];
|
||||
});
|
||||
}
|
||||
|
||||
function isBookFile(filePath: string): boolean {
|
||||
return [".epub", ".pdf", ".cbz", ".cbr"].includes(extname(filePath).toLowerCase());
|
||||
}
|
||||
|
||||
function bookFormatFromPath(filePath: string): "epub" | "pdf" | "cbz" | "cbr" {
|
||||
const extension = extname(filePath).toLowerCase();
|
||||
if (extension === ".epub") return "epub";
|
||||
if (extension === ".cbz") return "cbz";
|
||||
if (extension === ".cbr") return "cbr";
|
||||
return "pdf";
|
||||
}
|
||||
|
||||
@ -20,18 +20,33 @@ export class BooksService {
|
||||
if (query.q) {
|
||||
return this.search(query.q, query.limit, query.offset);
|
||||
}
|
||||
return this.database.db
|
||||
const statement = this.database.db
|
||||
.select()
|
||||
.from(books)
|
||||
.where(filters.length ? and(...filters) : undefined)
|
||||
.orderBy(books.title)
|
||||
.limit(query.limit)
|
||||
.offset(query.offset)
|
||||
.all()
|
||||
.orderBy(books.title);
|
||||
const rows =
|
||||
query.limit === undefined ? statement.all() : statement.limit(query.limit).offset(query.offset).all();
|
||||
return rows
|
||||
.map((book) => this.mapBookSelect(book));
|
||||
}
|
||||
|
||||
search(q: string, limit = 50, offset = 0) {
|
||||
search(q: string, limit?: number, offset = 0) {
|
||||
if (limit === undefined) {
|
||||
const rows = this.database.sqlite
|
||||
.prepare(
|
||||
`
|
||||
SELECT books.*
|
||||
FROM book_fts
|
||||
JOIN books ON books.id = book_fts.rowid
|
||||
WHERE book_fts MATCH ?
|
||||
ORDER BY bm25(book_fts)
|
||||
`
|
||||
)
|
||||
.all(`${q.replace(/"/g, '""')}*`);
|
||||
return (rows as Array<Record<string, unknown>>).map((row) => this.mapBookRow(row));
|
||||
}
|
||||
|
||||
const rows = this.database.sqlite
|
||||
.prepare(
|
||||
`
|
||||
|
||||
Reference in New Issue
Block a user