feat(metadata): write-to-file — embedded EPUB OPF + Calibre sidecars
Phase 5a of the metadata-writer feature: the book editor gains a "Write to file" action (admin) that persists the item's CURRENT database metadata into the artifact on disk, so the library stays portable to Calibre and other apps — the thing Save alone can't do. Dispatch (services/metadata_writer.go): EPUB/KEPUB gets its embedded OPF spliced in place (fresh dc:* block generated inside the ORIGINAL metadata open tag; manifest/spine/guide untouched; every other zip entry copied byte-identical in original order so mimetype stays first and STORED; original dc:identifiers preserved — the package's unique-identifier must keep resolving). Everything else takes a Calibre-style sidecar: metadata.opf when the book is ALONE in its directory (Calibre's own convention — round-trips through Calibre too), <basename>.opf when siblings share the folder, so one book's sidecar can never poison dir-mates' scans (the scanner now reads both, basename first). Safety contract: temp file + atomic rename — the original is never truncated on failure; the generated OPF is unit-tested to ROUND-TRIP through the scanner's own parser (write side and read side can never disagree); metadata_overrides are deliberately kept (file and DB agree after the write; overrides still guard against stale sidecars). The endpoint re-extracts inline (RescanMediaItem) and reports rescan failure separately — a failed rescan doesn't undo the write. SHA-256 changes are inherent and documented for users (Kobo/KOReader re-key). UI: the metadata editor modal carries the file path as a data attribute; the button confirms before mutating, labels the target (embedded vs sidecar) and toasts the outcome. Integration tests pin the 403, missing-file error, EPUB embed + post-write rescan title, the lone-book metadata.opf case, and the shared-dir <basename>.opf case with no dir-scoped litter.
This commit is contained in:
1 parent
61e5f47ec0
commit
eaf1b2c8dd
13 files changed
+1591
-413
No files matched your search
@@ -369,6 +369,32 @@ func (q *Queries) CountCollectionLiveItems(ctx context.Context, arg CountCollect
|
||||
return column_1, err
|
||||
}
|
||||
|
||||
const CountOtherMediaItemsInDir = `-- name: CountOtherMediaItemsInDir :one
|
||||
SELECT COUNT(*)::int
|
||||
FROM media_items
|
||||
WHERE library_id = $1::uuid
|
||||
AND id != $2::uuid
|
||||
AND archived_at IS NULL
|
||||
AND file_path LIKE ($3::text || '%')
|
||||
`
|
||||
|
||||
type CountOtherMediaItemsInDirParams struct {
|
||||
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
|
||||
ID pgtype.UUID `db:"id" json:"id"`
|
||||
DirPrefix pgtype.Text `db:"dir_prefix" json:"dir_prefix"`
|
||||
}
|
||||
|
||||
// Sibling-book count for sidecar naming (MetadataWriter): a dir-scoped
|
||||
// metadata.opf may only be written when NO other non-archived item
|
||||
// shares the directory; otherwise the sidecar goes to
|
||||
// <basename>.opf so one book's metadata can never poison dir-mates.
|
||||
func (q *Queries) CountOtherMediaItemsInDir(ctx context.Context, arg CountOtherMediaItemsInDirParams) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, CountOtherMediaItemsInDir, arg.LibraryID, arg.ID, arg.DirPrefix)
|
||||
var column_1 int32
|
||||
err := row.Scan(&column_1)
|
||||
return column_1, err
|
||||
}
|
||||
|
||||
const CountUnlinkedBooks = `-- name: CountUnlinkedBooks :one
|
||||
SELECT COUNT(*) as count
|
||||
FROM unlinked_books
|
||||
|
||||
Reference in new issue
Block a user