feat(api,shared): métadonnées multi-providers et automatisation des scans/enrichissements

Chaîne de résolution metadata (local, Open Library, Google Books, BNF)
avec activation/priorité/clé API par provider, colonnes isbn13 et
identifiers sur les livres, et intégration au scanner pour compléter
métadonnées et jaquettes manquantes. Module automation: réglages
persistés, planifications scan/enrichissement et déclenchement manuel,
exposés via des endpoints admin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Git Agent
2026-08-23 13:10:45 +02:00
parent 1ac4144d0e
commit 48e9459cf3
23 changed files with 1029 additions and 24 deletions

View File

@ -56,6 +56,8 @@ export class DatabaseService implements OnModuleDestroy {
author TEXT,
description TEXT,
isbn TEXT,
isbn13 TEXT,
identifiers_json TEXT,
language TEXT,
publisher TEXT,
published_date TEXT,
@ -89,6 +91,26 @@ export class DatabaseService implements OnModuleDestroy {
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS metadata_source_config (
provider TEXT PRIMARY KEY CHECK (provider IN ('local','openlibrary','googlebooks','bnf')),
enabled INTEGER NOT NULL DEFAULT 1,
priority INTEGER NOT NULL,
api_key TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS automation_settings (
id INTEGER PRIMARY KEY CHECK (id = 1),
watch_libraries INTEGER NOT NULL DEFAULT 0,
auto_enrich_new_books INTEGER NOT NULL DEFAULT 1,
isbn_priority_enabled INTEGER NOT NULL DEFAULT 1,
scan_schedule_json TEXT NOT NULL,
enrich_schedule_json TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE VIRTUAL TABLE IF NOT EXISTS book_fts USING fts5(
title,
author,
@ -100,6 +122,7 @@ export class DatabaseService implements OnModuleDestroy {
CREATE INDEX IF NOT EXISTS books_library_idx ON books(library_id);
CREATE INDEX IF NOT EXISTS books_title_idx ON books(title);
CREATE INDEX IF NOT EXISTS books_isbn13_idx ON books(isbn13);
CREATE INDEX IF NOT EXISTS jobs_status_idx ON jobs(status);
CREATE TRIGGER IF NOT EXISTS books_ai AFTER INSERT ON books BEGIN
@ -120,6 +143,8 @@ export class DatabaseService implements OnModuleDestroy {
END;
`);
this.ensureBooksSupportsComicArchives();
this.ensureBooksMetadataColumns();
this.ensureMetadataDefaults();
this.sqlite.exec("INSERT INTO book_fts(book_fts) VALUES('rebuild')");
}
@ -146,6 +171,8 @@ export class DatabaseService implements OnModuleDestroy {
author TEXT,
description TEXT,
isbn TEXT,
isbn13 TEXT,
identifiers_json TEXT,
language TEXT,
publisher TEXT,
published_date TEXT,
@ -158,11 +185,11 @@ export class DatabaseService implements OnModuleDestroy {
updated_at TEXT NOT NULL
);
INSERT INTO books (
id, library_id, title, author, description, isbn, language, publisher, published_date,
id, library_id, title, author, description, isbn, isbn13, identifiers_json, language, publisher, published_date,
format, file_path, cover_path, file_size, file_mtime, created_at, updated_at
)
SELECT
id, library_id, title, author, description, isbn, language, publisher, published_date,
id, library_id, title, author, description, isbn, NULL, NULL, language, publisher, published_date,
format, file_path, cover_path, file_size, file_mtime, created_at, updated_at
FROM books_legacy_format;
DROP TABLE books_legacy_format;
@ -174,6 +201,7 @@ export class DatabaseService implements OnModuleDestroy {
CREATE UNIQUE INDEX IF NOT EXISTS books_file_path_unique ON books(file_path);
CREATE INDEX IF NOT EXISTS books_library_idx ON books(library_id);
CREATE INDEX IF NOT EXISTS books_title_idx ON books(title);
CREATE INDEX IF NOT EXISTS books_isbn13_idx ON books(isbn13);
CREATE TRIGGER IF NOT EXISTS books_ai AFTER INSERT ON books BEGIN
INSERT INTO book_fts(rowid, title, author, description, isbn)
@ -193,4 +221,47 @@ export class DatabaseService implements OnModuleDestroy {
END;
`);
}
private ensureBooksMetadataColumns(): void {
const columns = this.sqlite.prepare("PRAGMA table_info(books)").all() as Array<{ name: string }>;
const names = new Set(columns.map((column) => column.name));
if (!names.has("isbn13")) {
this.sqlite.exec("ALTER TABLE books ADD COLUMN isbn13 TEXT");
}
if (!names.has("identifiers_json")) {
this.sqlite.exec("ALTER TABLE books ADD COLUMN identifiers_json TEXT");
}
this.sqlite.exec("CREATE INDEX IF NOT EXISTS books_isbn13_idx ON books(isbn13)");
}
private ensureMetadataDefaults(): void {
const now = this.now();
const insertSource = this.sqlite.prepare(`
INSERT INTO metadata_source_config (provider, enabled, priority, api_key, created_at, updated_at)
VALUES (?, ?, ?, NULL, ?, ?)
ON CONFLICT(provider) DO NOTHING
`);
insertSource.run("local", 1, 0, now, now);
insertSource.run("openlibrary", this.config.openLibraryEnabled ? 1 : 0, 1, now, now);
insertSource.run("googlebooks", 0, 2, now, now);
insertSource.run("bnf", 0, 3, now, now);
this.sqlite
.prepare(
`
INSERT INTO automation_settings (
id, watch_libraries, auto_enrich_new_books, isbn_priority_enabled,
scan_schedule_json, enrich_schedule_json, created_at, updated_at
)
VALUES (1, 0, 1, 1, ?, ?, ?, ?)
ON CONFLICT(id) DO NOTHING
`
)
.run(
JSON.stringify({ frequency: "disabled", time: "03:00", dayOfWeek: 1 }),
JSON.stringify({ frequency: "disabled", time: "04:00", dayOfWeek: 1 }),
now,
now
);
}
}

View File

@ -34,6 +34,8 @@ export const books = sqliteTable(
author: text("author"),
description: text("description"),
isbn: text("isbn"),
isbn13: text("isbn13"),
identifiersJson: text("identifiers_json"),
language: text("language"),
publisher: text("publisher"),
publishedDate: text("published_date"),
@ -75,3 +77,23 @@ export const jobs = sqliteTable("jobs", {
createdAt: text("created_at").notNull(),
updatedAt: text("updated_at").notNull()
});
export const metadataSourceConfig = sqliteTable("metadata_source_config", {
provider: text("provider", { enum: ["local", "openlibrary", "googlebooks", "bnf"] }).primaryKey(),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
priority: integer("priority").notNull(),
apiKey: text("api_key"),
createdAt: text("created_at").notNull(),
updatedAt: text("updated_at").notNull()
});
export const automationSettings = sqliteTable("automation_settings", {
id: integer("id").primaryKey(),
watchLibraries: integer("watch_libraries", { mode: "boolean" }).notNull().default(false),
autoEnrichNewBooks: integer("auto_enrich_new_books", { mode: "boolean" }).notNull().default(true),
isbnPriorityEnabled: integer("isbn_priority_enabled", { mode: "boolean" }).notNull().default(true),
scanScheduleJson: text("scan_schedule_json").notNull(),
enrichScheduleJson: text("enrich_schedule_json").notNull(),
createdAt: text("created_at").notNull(),
updatedAt: text("updated_at").notNull()
});