diff --git a/chat-summaries/2026-03-08_23-00-summary.md b/chat-summaries/2026-03-08_23-00-summary.md new file mode 100644 index 00000000..901d3d9b --- /dev/null +++ b/chat-summaries/2026-03-08_23-00-summary.md @@ -0,0 +1,46 @@ +# Parse and Display All Available EPUB Metadata Fields + Cleanup Refactor + +## Task +1. Extend the OPF parser, metadata cache, Epub accessors, i18n, and BookInfo screen to parse and display all standard Dublin Core metadata fields plus Calibre rating. +2. Refactor for code quality: consolidate duplicate static blank strings, add `getMetadata()` accessor, and simplify `buildLayout` to accept `BookMetadata` struct. + +## Changes Made + +### 1. ContentOpfParser (`lib/Epub/Epub/parsers/ContentOpfParser.h/.cpp`) +- Added 6 new `ParserState` entries: `IN_BOOK_PUBLISHER`, `IN_BOOK_DATE`, `IN_BOOK_SUBJECT`, `IN_BOOK_RIGHTS`, `IN_BOOK_CONTRIBUTOR`, `IN_BOOK_IDENTIFIER` +- Added 7 new public string members: `publisher`, `date`, `subjects`, `rights`, `contributor`, `identifier`, `rating` +- Added `identifierIsIsbn` flag for preferring ISBN identifiers over generic ones +- `startElement`: handles `dc:publisher`, `dc:date`, `dc:subject` (multi-tag, comma-separated), `dc:rights`, `dc:contributor` (multi-tag, comma-separated), `dc:identifier` (prefers `opf:scheme="ISBN"`), and `calibre:rating` meta tag +- `characterData`: appends text for all new states +- `endElement`: transitions back to `IN_METADATA` for all new `dc:*` elements + +### 2. BookMetadataCache (`lib/Epub/Epub/BookMetadataCache.h/.cpp`) +- Added 7 new fields to `BookMetadata` struct +- Bumped `BOOK_CACHE_VERSION` from 6 to 7 +- Updated `metadataSize` calculation (8 → 15 string fields) +- Added `writeString`/`readString` calls for all new fields in serialization/deserialization + +### 3. Epub Accessors (`lib/Epub/Epub.h/.cpp`) +- Added 7 new accessor methods: `getPublisher()`, `getDate()`, `getSubjects()`, `getRights()`, `getContributor()`, `getIdentifier()`, `getRating()` +- Added `getMetadata()` returning full `BookMetadataCache::BookMetadata` const ref +- Consolidated 13 duplicate `static std::string blank` locals into single file-scope `kBlank` (saves ~384 bytes DRAM) +- Propagated new fields from `opfParser` to `bookMetadata` in `parseContentOpf()` + +### 4. I18n (`lib/I18n/translations/english.yaml`, `lib/I18n/I18nKeys.h`) +- Added 7 new translation keys: `STR_PUBLISHER`, `STR_DATE`, `STR_SUBJECTS`, `STR_RATING`, `STR_ISBN`, `STR_RIGHTS`, `STR_CONTRIBUTOR` +- Regenerated I18n headers + +### 5. BookInfoActivity (`src/activities/home/BookInfoActivity.h/.cpp`) +- Refactored `buildLayout()` from 14 individual parameters to single `BookMetadataCache::BookMetadata` struct + `fileSize` +- `onEnter()` EPUB path uses `epub.getMetadata()` directly; XTC path builds a local `BookMetadata` +- Display order: Title, Author, Series, Publisher, Date, Subjects, Rating (N/5), Language, ISBN, Contributor, File Size, Rights, Description +- Rating displayed as `N / 5` (Calibre stores 0-10, divided by 2) + +## Commits +- `8025e6f` — `feat: parse and display all available EPUB metadata fields` +- (pending) — `refactor: consolidate Epub blank strings, simplify BookInfo buildLayout` + +## Follow-up +- Existing book caches will auto-invalidate (version 6 → 7) and regenerate on next load +- Users must delete `.crosspoint/` or let it regenerate to see new metadata for previously cached books +- Hardware testing needed to verify rendering of all new fields in all orientations diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index a2726a7e..1a8635e6 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -13,6 +13,11 @@ #include "Epub/parsers/TocNavParser.h" #include "Epub/parsers/TocNcxParser.h" +namespace { +const std::string kBlank; +const BookMetadataCache::BookMetadata kBlankMetadata; +} // namespace + bool Epub::findContentOpfFile(std::string* contentOpfFile) const { const auto containerPath = "META-INF/container.xml"; size_t containerSize; @@ -501,101 +506,75 @@ const std::string& Epub::getCachePath() const { return cachePath; } const std::string& Epub::getPath() const { return filepath; } const std::string& Epub::getTitle() const { - static std::string blank; - if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { - return blank; - } - + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank; return bookMetadataCache->coreMetadata.title; } const std::string& Epub::getAuthor() const { - static std::string blank; - if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { - return blank; - } - + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank; return bookMetadataCache->coreMetadata.author; } const std::string& Epub::getLanguage() const { - static std::string blank; - if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { - return blank; - } - + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank; return bookMetadataCache->coreMetadata.language; } const std::string& Epub::getSeries() const { - static std::string blank; - if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { - return blank; - } - + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank; return bookMetadataCache->coreMetadata.series; } const std::string& Epub::getSeriesIndex() const { - static std::string blank; - if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { - return blank; - } - + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank; return bookMetadataCache->coreMetadata.seriesIndex; } const std::string& Epub::getDescription() const { - static std::string blank; - if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { - return blank; - } - + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank; return bookMetadataCache->coreMetadata.description; } const std::string& Epub::getPublisher() const { - static std::string blank; - if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank; + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank; return bookMetadataCache->coreMetadata.publisher; } const std::string& Epub::getDate() const { - static std::string blank; - if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank; + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank; return bookMetadataCache->coreMetadata.date; } const std::string& Epub::getSubjects() const { - static std::string blank; - if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank; + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank; return bookMetadataCache->coreMetadata.subjects; } const std::string& Epub::getRights() const { - static std::string blank; - if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank; + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank; return bookMetadataCache->coreMetadata.rights; } const std::string& Epub::getContributor() const { - static std::string blank; - if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank; + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank; return bookMetadataCache->coreMetadata.contributor; } const std::string& Epub::getIdentifier() const { - static std::string blank; - if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank; + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank; return bookMetadataCache->coreMetadata.identifier; } const std::string& Epub::getRating() const { - static std::string blank; - if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank; + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank; return bookMetadataCache->coreMetadata.rating; } +const BookMetadataCache::BookMetadata& Epub::getMetadata() const { + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlankMetadata; + return bookMetadataCache->coreMetadata; +} + std::string Epub::getCoverBmpPath(bool cropped) const { const auto coverFileName = std::string("cover") + (cropped ? "_crop" : ""); return cachePath + "/" + coverFileName + ".bmp"; diff --git a/lib/Epub/Epub.h b/lib/Epub/Epub.h index 4acbad1e..80827b06 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -61,6 +61,7 @@ class Epub { const std::string& getContributor() const; const std::string& getIdentifier() const; const std::string& getRating() const; + const BookMetadataCache::BookMetadata& getMetadata() const; std::string getCoverBmpPath(bool cropped = false) const; bool generateCoverBmp(bool cropped = false) const; std::string getThumbBmpPath() const; diff --git a/src/activities/home/BookInfoActivity.cpp b/src/activities/home/BookInfoActivity.cpp index a3f8377c..9bd409a1 100644 --- a/src/activities/home/BookInfoActivity.cpp +++ b/src/activities/home/BookInfoActivity.cpp @@ -59,8 +59,7 @@ void BookInfoActivity::onEnter() { } } - std::string title, author, series, seriesIndex, description, language; - std::string publisher, date, subjects, rights, contributor, identifier, rating; + BookMetadataCache::BookMetadata meta; if (FsHelpers::hasEpubExtension(fileName)) { Epub epub(filePath, "/.crosspoint"); @@ -73,19 +72,8 @@ void BookInfoActivity::onEnter() { GUI.fillPopupProgress(renderer, popupRect, 50); } - title = epub.getTitle(); - author = epub.getAuthor(); - series = epub.getSeries(); - seriesIndex = epub.getSeriesIndex(); - description = normalizeWhitespace(epub.getDescription()); - language = epub.getLanguage(); - publisher = epub.getPublisher(); - date = epub.getDate(); - subjects = epub.getSubjects(); - rights = epub.getRights(); - contributor = epub.getContributor(); - identifier = epub.getIdentifier(); - rating = epub.getRating(); + meta = epub.getMetadata(); + meta.description = normalizeWhitespace(meta.description); const int coverH = renderer.getScreenHeight() * 2 / 5; if (epub.generateThumbBmp(coverH)) { @@ -93,8 +81,8 @@ void BookInfoActivity::onEnter() { } else { const int thumbW = static_cast(coverH * 0.6); const std::string placeholderPath = epub.getCachePath() + "/placeholder_" + std::to_string(coverH) + ".bmp"; - if (PlaceholderCoverGenerator::generate(placeholderPath, title.empty() ? fileName : title, author, thumbW, - coverH)) { + if (PlaceholderCoverGenerator::generate(placeholderPath, meta.title.empty() ? fileName : meta.title, meta.author, + thumbW, coverH)) { coverBmpPath = placeholderPath; } } @@ -113,8 +101,8 @@ void BookInfoActivity::onEnter() { if (needsBuild) { GUI.fillPopupProgress(renderer, popupRect, 50); } - title = xtc.getTitle(); - author = xtc.getAuthor(); + meta.title = xtc.getTitle(); + meta.author = xtc.getAuthor(); const int coverH = renderer.getScreenHeight() * 2 / 5; if (xtc.generateThumbBmp(coverH)) { @@ -122,8 +110,8 @@ void BookInfoActivity::onEnter() { } else { const int thumbW = static_cast(coverH * 0.6); const std::string placeholderPath = xtc.getCachePath() + "/placeholder_" + std::to_string(coverH) + ".bmp"; - if (PlaceholderCoverGenerator::generate(placeholderPath, title.empty() ? fileName : title, author, thumbW, - coverH)) { + if (PlaceholderCoverGenerator::generate(placeholderPath, meta.title.empty() ? fileName : meta.title, meta.author, + thumbW, coverH)) { coverBmpPath = placeholderPath; } } @@ -133,23 +121,17 @@ void BookInfoActivity::onEnter() { } } - if (title.empty()) { - title = fileName; + if (meta.title.empty()) { + meta.title = fileName; } - buildLayout(title, author, series, seriesIndex, description, language, fileSize, publisher, date, subjects, rights, - contributor, identifier, rating); + buildLayout(meta, fileSize); requestUpdate(); } void BookInfoActivity::onExit() { Activity::onExit(); } -void BookInfoActivity::buildLayout(const std::string& title, const std::string& author, const std::string& series, - const std::string& seriesIndex, const std::string& description, - const std::string& language, size_t fileSize, const std::string& publisher, - const std::string& date, const std::string& subjects, const std::string& rights, - const std::string& contributor, const std::string& identifier, - const std::string& rating) { +void BookInfoActivity::buildLayout(const BookMetadataCache::BookMetadata& meta, size_t fileSize) { const int contentW = renderer.getScreenWidth() - MARGIN * 2; fields.reserve(13); @@ -162,23 +144,23 @@ void BookInfoActivity::buildLayout(const std::string& title, const std::string& fields.push_back(std::move(field)); }; - addField(nullptr, title, true, EpdFontFamily::BOLD); - addField(tr(STR_AUTHOR), author, false, EpdFontFamily::REGULAR); + addField(nullptr, meta.title, true, EpdFontFamily::BOLD); + addField(tr(STR_AUTHOR), meta.author, false, EpdFontFamily::REGULAR); - if (!series.empty()) { - std::string seriesStr = series; - if (!seriesIndex.empty()) { - seriesStr += " #" + seriesIndex; + if (!meta.series.empty()) { + std::string seriesStr = meta.series; + if (!meta.seriesIndex.empty()) { + seriesStr += " #" + meta.seriesIndex; } addField(tr(STR_SERIES), seriesStr, false, EpdFontFamily::REGULAR); } - addField(tr(STR_PUBLISHER), publisher, false, EpdFontFamily::REGULAR); - addField(tr(STR_DATE), date, false, EpdFontFamily::REGULAR); - addField(tr(STR_SUBJECTS), subjects, false, EpdFontFamily::REGULAR); + addField(tr(STR_PUBLISHER), meta.publisher, false, EpdFontFamily::REGULAR); + addField(tr(STR_DATE), meta.date, false, EpdFontFamily::REGULAR); + addField(tr(STR_SUBJECTS), meta.subjects, false, EpdFontFamily::REGULAR); - if (!rating.empty()) { - int ratingVal = atoi(rating.c_str()); + if (!meta.rating.empty()) { + int ratingVal = atoi(meta.rating.c_str()); if (ratingVal > 0 && ratingVal <= 10) { char ratingBuf[8]; snprintf(ratingBuf, sizeof(ratingBuf), "%d / 5", (ratingVal + 1) / 2); @@ -186,16 +168,16 @@ void BookInfoActivity::buildLayout(const std::string& title, const std::string& } } - addField(tr(STR_LANGUAGE), language, false, EpdFontFamily::REGULAR); - addField(tr(STR_ISBN), identifier, false, EpdFontFamily::REGULAR); - addField(tr(STR_CONTRIBUTOR), contributor, false, EpdFontFamily::REGULAR); + addField(tr(STR_LANGUAGE), meta.language, false, EpdFontFamily::REGULAR); + addField(tr(STR_ISBN), meta.identifier, false, EpdFontFamily::REGULAR); + addField(tr(STR_CONTRIBUTOR), meta.contributor, false, EpdFontFamily::REGULAR); if (fileSize > 0) { addField(tr(STR_FILE_SIZE), formatFileSize(fileSize), false, EpdFontFamily::REGULAR); } - addField(tr(STR_RIGHTS), rights, false, EpdFontFamily::REGULAR); - addField(tr(STR_DESCRIPTION), description, false, EpdFontFamily::REGULAR); + addField(tr(STR_RIGHTS), meta.rights, false, EpdFontFamily::REGULAR); + addField(tr(STR_DESCRIPTION), meta.description, false, EpdFontFamily::REGULAR); const int lineH10 = renderer.getLineHeight(UI_10_FONT_ID); const int lineH12 = renderer.getLineHeight(UI_12_FONT_ID); diff --git a/src/activities/home/BookInfoActivity.h b/src/activities/home/BookInfoActivity.h index 9de65c77..50cc4ad6 100644 --- a/src/activities/home/BookInfoActivity.h +++ b/src/activities/home/BookInfoActivity.h @@ -3,6 +3,8 @@ #include #include +#include + #include "../Activity.h" class BookInfoActivity final : public Activity { @@ -31,10 +33,6 @@ class BookInfoActivity final : public Activity { int coverDisplayHeight = 0; int coverDisplayWidth = 0; - void buildLayout(const std::string& title, const std::string& author, const std::string& series, - const std::string& seriesIndex, const std::string& description, const std::string& language, - size_t fileSize, const std::string& publisher, const std::string& date, - const std::string& subjects, const std::string& rights, const std::string& contributor, - const std::string& identifier, const std::string& rating); + void buildLayout(const BookMetadataCache::BookMetadata& meta, size_t fileSize); static std::string formatFileSize(size_t bytes); };