#!/usr/bin/env python3 from __future__ import annotations import argparse import json import os import sqlite3 from datetime import UTC, datetime from pathlib import Path from curator.db import Database, normalize from curator.epub import inspect_epub from curator.library import safe_component, safe_filename def main() -> None: parser = argparse.ArgumentParser(description="Repair a Curator EPUB from its embedded metadata") parser.add_argument("database", type=Path) parser.add_argument("asset_id", type=int) parser.add_argument("--apply", action="store_true") args = parser.parse_args() connection = sqlite3.connect(args.database) connection.row_factory = sqlite3.Row row = connection.execute( """SELECT a.*, e.id AS edition_id, e.work_id, e.variant FROM assets a JOIN editions e ON e.id=a.edition_id WHERE a.id=?""", (args.asset_id,), ).fetchone() if not row or row["format"] != "epub": raise SystemExit("asset must be an existing EPUB") old_path = Path(row["path"]) info = inspect_epub(old_path) filename = safe_filename(row["filename"], "book.epub") root = old_path.parents[3] destination = ( root / safe_component(info.author, "Unknown Author") / safe_component(info.title, "Untitled") / safe_component(f"{info.language}-{row['variant']}", "und-original") / filename ) metadata = json.loads(row["metadata_json"] or "{}") metadata.update({ "display_title": info.display_title, "title_aliases": list(info.title_aliases), "declared_language": info.declared_language, "identifiers": list(info.identifiers), "source_identifiers": list(info.source_identifiers), }) plan = {"work_id": row["work_id"], "asset_id": row["id"], "title": info.title, "author": info.author, "language": info.language, "isbn": info.isbn, "old": str(old_path), "new": str(destination)} print(json.dumps(plan, ensure_ascii=False, indent=2)) if not args.apply: return backup = args.database.with_name(f"{args.database.stem}-before-repair-{datetime.now(UTC).strftime('%Y%m%dT%H%M%SZ')}.sqlite3") with sqlite3.connect(backup) as backup_connection: connection.backup(backup_connection) moved = False try: connection.execute("PRAGMA foreign_keys=ON") connection.execute("BEGIN IMMEDIATE") destination.parent.mkdir(parents=True, exist_ok=True) if destination.exists() and destination != old_path: raise FileExistsError(destination) if destination != old_path: os.replace(old_path, destination) moved = True stamp = datetime.now(UTC).replace(microsecond=0).isoformat() connection.execute( """UPDATE works SET title=?, author=?, normalized_title=?, normalized_author=?, updated_at=? WHERE id=?""", (info.title, info.author, normalize(info.title), normalize(info.author), stamp, row["work_id"]), ) connection.execute( "UPDATE editions SET language=?, isbn=?, publisher=? WHERE id=?", (info.language, info.isbn, info.publisher, row["edition_id"]), ) connection.execute( "UPDATE assets SET filename=?, path=?, metadata_json=? WHERE id=?", (filename, str(destination), json.dumps(metadata, ensure_ascii=False, sort_keys=True), row["id"]), ) connection.execute( "UPDATE activity_jobs SET detail=?, updated_at=? WHERE kind='book-import' AND detail LIKE '%�%'", (f"{info.title} · {info.author or '未知作者'} · 已校验并入库", stamp), ) connection.commit() except Exception: connection.rollback() if moved: old_path.parent.mkdir(parents=True, exist_ok=True) os.replace(destination, old_path) raise finally: connection.close() Database(args.database).reconcile_imported_book(info.title, info.author) print(f"backup={backup}") if __name__ == "__main__": main()