Resolved metadata
resolveArchive (and the lower-level resolveMetadata) normalize every metadata source a container can carry into one cross-format vocabulary: ResolvedMetadata. This page is the reference for that vocabulary, the per-format mapping tables, and the precedence rules when several sources speak.
Design rules
Section titled “Design rules”- Union schema, not intersection. The shape is rich enough to express what any supported format can say; each source populates the subset it knows. Results are sparse: an absent key means "no source had anything to say", and empty arrays/strings collapse to absent so
field !== undefinedis a reliable presence check. - Vocabulary anchored on prior art. Field names and semantics follow the Readium Web Publication Manifest where a term exists:
contributorswith role terms,belongsTo.series/belongsTo.collectionwithposition,numberOfPages, publication details,description… - Normalization ≠ convergence. Every field is typed/parsed/validated (
BlackAndWhite: "Yes"→boolean,Year/Month/Day→ a date shape), but concepts with no cross-format twin live in clearly format-scoped corners (metadata.comicInfo,metadata.apple,metadata.kobo) rather than behind faked generic names. - Losslessness: sources are never load-bearing. Everything a parser captures lands somewhere in the resolved entity, and the tables below say where. Two compile-time checks keep them from drifting: each resolver names every field it was handed (a parser gaining one fails the build until someone decides what becomes of it), and each format-scoped corner is built against
Exhaustive<T>, so a new field in the vocabulary fails the build in every producer. For the open-ended OPFmetavocabulary, every entry is structurally copied intometadata.properties, lossless by construction. metadataversussources.metadatasays what the resolver believes;sources(the verbatim parser outputs) say what the book said. The duplication is the contract: a wrong mapping opinion is revisable in a release because the raw value never left the entity.
The vocabulary
Section titled “The vocabulary”/** "derived" = declared or properly computed; "assumed" = a best-effort convention */type ResolvedConfidence = "derived" | "assumed"
type Collection = { name?: string /** EPUB `dcterms:identifier` refinement of the collection, catalog series ids */ identifiers?: { value: string; scheme: string }[] position?: number total?: number}
type ResolvedCover = { /** container-relative uri (rebase like reading-order uris), or an absolute url from a remote catalog */ uri: string mediaType?: string confidence: ResolvedConfidence}
type ResolvedMetadata = { /** every stated title, in document order — the first is the main title (`mainTitle(metadata)`) */ titles?: { value: string type?: "main" | "subtitle" | "short" | "collection" | "edition" | "expanded" | (string & {}) displaySeq?: number sortAs?: string }[] /** the cover image; resolved against the container, so `resolveArchive` only */ cover?: ResolvedCover description?: string /** the work's first publication and this concrete edition are distinct facts */ publication?: { original?: { date?: { year?: number; month?: number; day?: number } publisher?: string imprint?: string } edition?: { date?: { year?: number; month?: number; day?: number } publisher?: string imprint?: string } } rights?: string languages?: string[] subjects?: string[] contributors?: { name: string; roles: string[]; sortAs?: string }[] readingDirection?: "ltr" | "rtl" renditionLayout?: "reflowable" | "pre-paginated" renditionFlow?: "scrolled-continuous" | "scrolled-doc" | "paginated" | "auto" renditionSpread?: "none" | "landscape" | "portrait" | "both" | "auto" /** declared count, else counted page-like pages; `resolveArchive` only when counted */ numberOfPages?: number /** aggregate community score, 0–5 (schema.org `aggregateRating`) */ aggregateRating?: { value: number; count?: number } /** every stated identifier — `identifierValue(metadata.identifiers, "ISBN")` reads a specific one out */ identifiers?: { value: string scheme: "ISBN" | "GTIN" | "DOI" | "GoogleBooks" | "OpenLibrary" | "ProjectGutenberg" | "URL" | "Unknown" | (string & {}) unique?: true }[] /** a collection is identified by a name, by identifiers, or by both — sources differ */ belongsTo?: { series?: Collection[] collection?: Collection[] } /** open-world channel: every OPF <meta>, verbatim */ properties?: { property?: string; refines?: string; name?: string; content?: string; value?: string }[] /** format-scoped corners */ comicInfo?: ResolvedComicInfoMetadata apple?: { fixedLayout?: boolean; options?: { name?: string; value: string }[] } kobo?: { fixedLayout?: boolean }}Cover and page count
Section titled “Cover and page count”Two fields are resolved for the whole container, not just its descriptive sidecars, so they are populated by resolveArchive and left absent by the source-level resolveMetadata / resolveArchiveMetadata, which never see the file listing:
cover— the OPF-declared cover image (EPUB 3cover-imageproperty, the EPUB 2<meta name="cover">convention, or acover-ish image id), rebased onto a container-relativeuri(confidence: "derived"); else the first image of the reading order for image-content containers — comics, image archives, synthetic image-spine OPFs such ascreateArchiveFromUrlslists (confidence: "assumed"). It stays absent when nothing image-like is a reading resource: an authored reflowable EPUB (text spine, so an interior illustration is never promoted) or an audiobook/video archive has no cover — a container with no cover of its own is exactly the case metadata-fetcher answers, with an absolute url into the catalog's cover service.numberOfPages— the declared ComicInfoPageCount, else the count of page-like reading-order items (images and fixed-layout documents). Reflowable documents are not pages, and audio/video tracks are not pages, so both are excluded: a reflowable book or an audiobook has no page count.
Aggregate rating
Section titled “Aggregate rating”aggregateRating is an aggregate community score on a 0–5 star scale — the scale every source stating one uses (ComicInfo CommunityRating, and the catalogs behind metadata-fetcher). A source on another scale is rescaled by its resolver, exactly like MARC language codes and W3C-DTF dates; which source stated it is already structural (sources.* here, providerId on a fetched match), so the original scale is never needed to interpret the value. count is the number of ratings the mean is over, absent when the source doesn't state one — ComicInfo never does.
A score outside 0–5, or one announced over zero ratings, is dropped instead of passed on: it would render as a real star count.
Two things deliberately do not land here:
- EPUB has no rating. The specification defines no rating property — not in DCMES, not in the package metadata vocabulary — so an OPF contributes none. An authored one (
calibre:rating, or aschema:aggregateRatingmeta through the reservedschemaprefix) arrives verbatim inproperties. - A personal score is not an aggregate. calibre's
calibre:ratingis one owner's own rating on its own 0–10 scale: a different fact about a different subject, so it stays inpropertiesrather than being folded in.
Publication details
Section titled “Publication details”publication.original describes the work's first known publication. publication.edition describes the concrete publication represented by the current file or catalog item. Keeping each event's date, publisher, and imprint together prevents an EPUB edition date, an original print date, and a catalog site's release from being treated as interchangeable.
An EPUB package's dc:date is the publication date of that specific EPUB publication, so OPF and ComicInfo values populate publication.edition. Catalog providers can additionally populate publication.original when their source explicitly identifies the first publication.
Contributor roles
Section titled “Contributor roles”Roles use the Readium terms our sources can express — author, translator, editor, artist, illustrator, letterer, penciler, colorist, inker, narrator, contributor — plus the prose-reader extension coverArtist (ComicInfo CoverArtist; Readium has no cover-art term). Well-known MARC relator codes from OPF (aut, trl, edt, art, ill, clr, nrt, ctb) are normalized to those terms; unknown tokens pass through verbatim. A role-less dc:creator defaults to author, a role-less dc:contributor to contributor.
Identifiers
Section titled “Identifiers”Every non-empty OPF dc:identifier is preserved in document order. The identifier selected by package@unique-identifier has unique: true. Parsed source data at sources.opf.identifiers also retains each element's XML id; normalized metadata.identifiers omits that internal anchor while keeping the semantic marker.
Normalized identifiers retain an explicitly authored EPUB 2 opf:scheme, or the value of an EPUB 3 meta property="identifier-type" that refines the identifier. Types expressed through scheme="onix:codelist5" are normalized to their named identifier system (06 → DOI, 15 → ISBN, and the other standard codes used by the resolver). Known scheme spellings are canonicalized while custom strings remain valid. The direct EPUB 2 attribute wins when a hybrid file states both. Without an authored type, recognizable ISBN and GTIN values become ISBN/GTIN, a valid absolute HTTP(S) value becomes URL, and the lossless fallback is Unknown.
The shared MetadataIdentifier, MetadataIdentifierScheme, and KnownMetadataIdentifierScheme types are exported. ResolvedMetadataIdentifier adds only EPUB's optional unique marker. Reading one identifier out of the list is a derivation, not a field: identifierValue(metadata.identifiers, scheme) is exported for it — use it rather than re-deriving the rule, which has to accept both the ISBN and GTIN schemes for either, reject a non-book barcode, and read a catalog id out of the reference URL that carries it. See Publication identifiers for the complete resolution model, with Google Books and Project Gutenberg as concrete catalog examples.
Precedence
Section titled “Precedence”When several sources are present, resolveMetadata merges field-wise:
| Field | Rule |
|---|---|
descriptive fields (titles, description, languages, subjects, contributors, belongsTo) |
OPF wins over ComicInfo — the package document is the publication's own metadata; the sidecar fills gaps |
publication.edition details (date, publisher, imprint) |
merged field-wise, OPF wins over ComicInfo and the sidecar fills gaps |
readingDirection |
ComicInfo wins over OPF (Manga beats page-progression-direction) — deliberate, preserving the historical pipeline behavior |
renditionLayout |
OPF explicit → Apple → Kobo, first defined wins; the layoutScan promotion applies on top, inside the resolver |
identifiers |
concatenated, OPF first (identifier systems coexist) |
cover |
OPF-declared cover, else the first-image fallback for image-content containers (resolveArchive only) |
numberOfPages |
declared ComicInfo PageCount, else the counted page-like reading-order items (resolveArchive only) |
corners (comicInfo, apple, kobo) and single-source fields (rights, properties, aggregateRating) |
from their only producer |
Mapping tables
Section titled “Mapping tables”These mirror the compile-enforced tables shipped next to each resolver — the losslessness audit trail. Dotted paths address the scoped corners; readingOrder/toc mean the field feeds a sibling part of the resolved entity; cover is the metadata.cover field (resolved against the container by resolveArchive); guide is a reserved home still only reachable through sources.
ComicInfo → metadata
Section titled “ComicInfo → metadata”| ComicInfo field | Home | Notes |
|---|---|---|
Title |
titles |
trimmed; the sidecar states one title, so titles has the single entry |
Summary |
description |
|
Publisher |
publication.edition.publisher |
|
Imprint |
publication.edition.imprint |
|
LanguageISO |
languages |
single-entry array |
Genre, Tags |
subjects |
comma-split, Genre first |
Writer |
contributors |
role author, comma-split |
Penciller |
contributors |
role penciler |
Inker |
contributors |
role inker |
Colorist |
contributors |
role colorist |
Letterer |
contributors |
role letterer |
CoverArtist |
contributors |
role coverArtist |
Editor |
contributors |
role editor |
Translator |
contributors |
role translator |
Year, Month, Day |
publication.edition.date |
independent optional components |
Manga |
readingDirection |
YesAndRightToLeft → rtl; also comicInfo.manga |
PageCount |
numberOfPages |
declared count; a page-like container without it is counted at the archive level |
GTIN |
identifiers |
scheme GTIN, the field being broader than an ISBN; identifierValue(…, "ISBN") still reads an ISBN-shaped value out of it |
Series, Number, Count |
belongsTo.series |
name / position / total; Number or Count without a Series still states this issue's place, so the entry is kept without a name |
SeriesGroup |
belongsTo.collection |
comma-split |
AlternateSeries, AlternateNumber, AlternateCount |
comicInfo.alternateSeries |
|
StoryArc, StoryArcNumber |
comicInfo.storyArcs |
comma-split, zipped positionally |
BlackAndWhite |
comicInfo.blackAndWhite |
Yes/No → boolean |
Volume |
comicInfo.volume |
numeric |
Format |
comicInfo.format |
|
AgeRating |
comicInfo.ageRating |
|
CommunityRating |
aggregateRating |
the schema's 0–5 mean, already the shared scale; no count is stated, and a value out of range is dropped |
Notes |
comicInfo.notes |
|
Review |
comicInfo.review |
|
Web |
comicInfo.web, identifiers |
whitespace-split reference list; valid absolute HTTP(S) values also become scheme URL identifiers |
ScanInformation |
comicInfo.scanInformation |
|
MainCharacterOrTeam |
comicInfo.mainCharacterOrTeam |
|
Characters |
comicInfo.characters |
comma-split |
Teams |
comicInfo.teams |
comma-split |
Locations |
comicInfo.locations |
comma-split |
OPF → metadata
Section titled “OPF → metadata”| OPF field | Home | Notes |
|---|---|---|
dc:title |
titles |
every non-empty one, in document order; EPUB 3 makes the first the main title whatever a later title-type claims — mainTitle(metadata) reads it |
title-type, display-seq, file-as refinements |
titles[].type / displaySeq / sortAs |
type lowercased, unknown values verbatim |
dc:description |
description |
|
dc:publisher |
publication.edition.publisher |
|
dc:rights |
rights |
|
dc:language |
languages |
all, document order |
dc:subject |
subjects |
all, document order |
dc:creator, dc:contributor |
contributors |
roles from opf:role and EPUB 3 meta refines property="role", file-as → sortAs |
dc:date |
publication.edition.date |
the specific EPUB publication's date, parsed as W3CDTF |
dc:identifier |
identifiers |
all; EPUB 2 opf:scheme or EPUB 3 identifier-type refinement retained, otherwise recognizable ISBN/GTIN, valid absolute HTTP(S) → URL, or Unknown; the package@unique-identifier target has unique: true |
spine page-progression-direction |
readingDirection |
validated |
rendition:layout meta |
renditionLayout |
validated |
rendition:flow meta |
renditionFlow |
validated |
rendition:spread meta |
renditionSpread |
validated |
belongs-to-collection metas |
belongsTo |
collection-type series → series (with group-position), else collection |
dcterms:identifier refinement of a collection |
belongsTo.*[].identifiers |
scheme from an identifier-type refinement of that identifier, else inferred |
calibre:series / calibre:series_index |
belongsTo.series |
fallback when no EPUB 3 series meta |
every <meta> |
properties |
verbatim, the open-world channel |
| manifest items, spine itemrefs | readingOrder |
structural |
spine toc idref |
toc |
structural |
cover heuristics (coverHref) |
cover |
the metadata.cover field; basePath-resolved by resolveArchive, confidence: "derived" |
<guide> |
guide |
reserved — sources.opf only today |
Apple / Kobo → metadata
Section titled “Apple / Kobo → metadata”| Source field | Home | Notes |
|---|---|---|
Apple fixed-layout option |
apple.fixedLayout + renditionLayout |
true → pre-paginated |
| Apple display options (all) | apple.options |
verbatim, lossless |
Kobo fixed-layout option |
kobo.fixedLayout + renditionLayout |
true → pre-paginated |
Error policy & versioning
Section titled “Error policy & versioning”Per-source parse failures are swallowed and logged via the debug Report (books in the wild are dirty): a malformed ComicInfo.xml never fails a resolve, it just doesn't contribute. The ResolvedArchive entity carries a version field for consumers persisting it — currently 3, reflecting the normalized identifier model. It is bumped only when the shape or meaning of existing fields changes incompatibly; additive vocabulary growth does not bump it.