refactor: consolidate Epub blank strings, simplify BookInfo buildLayout

Replace 13 per-accessor static std::string blank locals with a single
file-scope kBlank (~384 bytes DRAM saved). Add Epub::getMetadata()
returning the full BookMetadata struct. Refactor buildLayout from 14
individual parameters to a single BookMetadata const ref + fileSize.

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-09 02:56:35 -04:00
parent 8025e6fb0d
commit efa727eff2
5 changed files with 102 additions and 96 deletions

View File

@@ -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

View File

@@ -13,6 +13,11 @@
#include "Epub/parsers/TocNavParser.h" #include "Epub/parsers/TocNavParser.h"
#include "Epub/parsers/TocNcxParser.h" #include "Epub/parsers/TocNcxParser.h"
namespace {
const std::string kBlank;
const BookMetadataCache::BookMetadata kBlankMetadata;
} // namespace
bool Epub::findContentOpfFile(std::string* contentOpfFile) const { bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
const auto containerPath = "META-INF/container.xml"; const auto containerPath = "META-INF/container.xml";
size_t containerSize; 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::getPath() const { return filepath; }
const std::string& Epub::getTitle() const { const std::string& Epub::getTitle() const {
static std::string blank; if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank;
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
return blank;
}
return bookMetadataCache->coreMetadata.title; return bookMetadataCache->coreMetadata.title;
} }
const std::string& Epub::getAuthor() const { const std::string& Epub::getAuthor() const {
static std::string blank; if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank;
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
return blank;
}
return bookMetadataCache->coreMetadata.author; return bookMetadataCache->coreMetadata.author;
} }
const std::string& Epub::getLanguage() const { const std::string& Epub::getLanguage() const {
static std::string blank; if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank;
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
return blank;
}
return bookMetadataCache->coreMetadata.language; return bookMetadataCache->coreMetadata.language;
} }
const std::string& Epub::getSeries() const { const std::string& Epub::getSeries() const {
static std::string blank; if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank;
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
return blank;
}
return bookMetadataCache->coreMetadata.series; return bookMetadataCache->coreMetadata.series;
} }
const std::string& Epub::getSeriesIndex() const { const std::string& Epub::getSeriesIndex() const {
static std::string blank; if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank;
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
return blank;
}
return bookMetadataCache->coreMetadata.seriesIndex; return bookMetadataCache->coreMetadata.seriesIndex;
} }
const std::string& Epub::getDescription() const { const std::string& Epub::getDescription() const {
static std::string blank; if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank;
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
return blank;
}
return bookMetadataCache->coreMetadata.description; return bookMetadataCache->coreMetadata.description;
} }
const std::string& Epub::getPublisher() const { const std::string& Epub::getPublisher() const {
static std::string blank; if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank;
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank;
return bookMetadataCache->coreMetadata.publisher; return bookMetadataCache->coreMetadata.publisher;
} }
const std::string& Epub::getDate() const { const std::string& Epub::getDate() const {
static std::string blank; if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank;
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank;
return bookMetadataCache->coreMetadata.date; return bookMetadataCache->coreMetadata.date;
} }
const std::string& Epub::getSubjects() const { const std::string& Epub::getSubjects() const {
static std::string blank; if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank;
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank;
return bookMetadataCache->coreMetadata.subjects; return bookMetadataCache->coreMetadata.subjects;
} }
const std::string& Epub::getRights() const { const std::string& Epub::getRights() const {
static std::string blank; if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank;
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank;
return bookMetadataCache->coreMetadata.rights; return bookMetadataCache->coreMetadata.rights;
} }
const std::string& Epub::getContributor() const { const std::string& Epub::getContributor() const {
static std::string blank; if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank;
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank;
return bookMetadataCache->coreMetadata.contributor; return bookMetadataCache->coreMetadata.contributor;
} }
const std::string& Epub::getIdentifier() const { const std::string& Epub::getIdentifier() const {
static std::string blank; if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank;
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank;
return bookMetadataCache->coreMetadata.identifier; return bookMetadataCache->coreMetadata.identifier;
} }
const std::string& Epub::getRating() const { const std::string& Epub::getRating() const {
static std::string blank; if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return kBlank;
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank;
return bookMetadataCache->coreMetadata.rating; 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 { std::string Epub::getCoverBmpPath(bool cropped) const {
const auto coverFileName = std::string("cover") + (cropped ? "_crop" : ""); const auto coverFileName = std::string("cover") + (cropped ? "_crop" : "");
return cachePath + "/" + coverFileName + ".bmp"; return cachePath + "/" + coverFileName + ".bmp";

View File

@@ -61,6 +61,7 @@ class Epub {
const std::string& getContributor() const; const std::string& getContributor() const;
const std::string& getIdentifier() const; const std::string& getIdentifier() const;
const std::string& getRating() const; const std::string& getRating() const;
const BookMetadataCache::BookMetadata& getMetadata() const;
std::string getCoverBmpPath(bool cropped = false) const; std::string getCoverBmpPath(bool cropped = false) const;
bool generateCoverBmp(bool cropped = false) const; bool generateCoverBmp(bool cropped = false) const;
std::string getThumbBmpPath() const; std::string getThumbBmpPath() const;

View File

@@ -59,8 +59,7 @@ void BookInfoActivity::onEnter() {
} }
} }
std::string title, author, series, seriesIndex, description, language; BookMetadataCache::BookMetadata meta;
std::string publisher, date, subjects, rights, contributor, identifier, rating;
if (FsHelpers::hasEpubExtension(fileName)) { if (FsHelpers::hasEpubExtension(fileName)) {
Epub epub(filePath, "/.crosspoint"); Epub epub(filePath, "/.crosspoint");
@@ -73,19 +72,8 @@ void BookInfoActivity::onEnter() {
GUI.fillPopupProgress(renderer, popupRect, 50); GUI.fillPopupProgress(renderer, popupRect, 50);
} }
title = epub.getTitle(); meta = epub.getMetadata();
author = epub.getAuthor(); meta.description = normalizeWhitespace(meta.description);
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();
const int coverH = renderer.getScreenHeight() * 2 / 5; const int coverH = renderer.getScreenHeight() * 2 / 5;
if (epub.generateThumbBmp(coverH)) { if (epub.generateThumbBmp(coverH)) {
@@ -93,8 +81,8 @@ void BookInfoActivity::onEnter() {
} else { } else {
const int thumbW = static_cast<int>(coverH * 0.6); const int thumbW = static_cast<int>(coverH * 0.6);
const std::string placeholderPath = epub.getCachePath() + "/placeholder_" + std::to_string(coverH) + ".bmp"; const std::string placeholderPath = epub.getCachePath() + "/placeholder_" + std::to_string(coverH) + ".bmp";
if (PlaceholderCoverGenerator::generate(placeholderPath, title.empty() ? fileName : title, author, thumbW, if (PlaceholderCoverGenerator::generate(placeholderPath, meta.title.empty() ? fileName : meta.title, meta.author,
coverH)) { thumbW, coverH)) {
coverBmpPath = placeholderPath; coverBmpPath = placeholderPath;
} }
} }
@@ -113,8 +101,8 @@ void BookInfoActivity::onEnter() {
if (needsBuild) { if (needsBuild) {
GUI.fillPopupProgress(renderer, popupRect, 50); GUI.fillPopupProgress(renderer, popupRect, 50);
} }
title = xtc.getTitle(); meta.title = xtc.getTitle();
author = xtc.getAuthor(); meta.author = xtc.getAuthor();
const int coverH = renderer.getScreenHeight() * 2 / 5; const int coverH = renderer.getScreenHeight() * 2 / 5;
if (xtc.generateThumbBmp(coverH)) { if (xtc.generateThumbBmp(coverH)) {
@@ -122,8 +110,8 @@ void BookInfoActivity::onEnter() {
} else { } else {
const int thumbW = static_cast<int>(coverH * 0.6); const int thumbW = static_cast<int>(coverH * 0.6);
const std::string placeholderPath = xtc.getCachePath() + "/placeholder_" + std::to_string(coverH) + ".bmp"; const std::string placeholderPath = xtc.getCachePath() + "/placeholder_" + std::to_string(coverH) + ".bmp";
if (PlaceholderCoverGenerator::generate(placeholderPath, title.empty() ? fileName : title, author, thumbW, if (PlaceholderCoverGenerator::generate(placeholderPath, meta.title.empty() ? fileName : meta.title, meta.author,
coverH)) { thumbW, coverH)) {
coverBmpPath = placeholderPath; coverBmpPath = placeholderPath;
} }
} }
@@ -133,23 +121,17 @@ void BookInfoActivity::onEnter() {
} }
} }
if (title.empty()) { if (meta.title.empty()) {
title = fileName; meta.title = fileName;
} }
buildLayout(title, author, series, seriesIndex, description, language, fileSize, publisher, date, subjects, rights, buildLayout(meta, fileSize);
contributor, identifier, rating);
requestUpdate(); requestUpdate();
} }
void BookInfoActivity::onExit() { Activity::onExit(); } void BookInfoActivity::onExit() { Activity::onExit(); }
void BookInfoActivity::buildLayout(const std::string& title, const std::string& author, const std::string& series, void BookInfoActivity::buildLayout(const BookMetadataCache::BookMetadata& meta, size_t fileSize) {
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) {
const int contentW = renderer.getScreenWidth() - MARGIN * 2; const int contentW = renderer.getScreenWidth() - MARGIN * 2;
fields.reserve(13); fields.reserve(13);
@@ -162,23 +144,23 @@ void BookInfoActivity::buildLayout(const std::string& title, const std::string&
fields.push_back(std::move(field)); fields.push_back(std::move(field));
}; };
addField(nullptr, title, true, EpdFontFamily::BOLD); addField(nullptr, meta.title, true, EpdFontFamily::BOLD);
addField(tr(STR_AUTHOR), author, false, EpdFontFamily::REGULAR); addField(tr(STR_AUTHOR), meta.author, false, EpdFontFamily::REGULAR);
if (!series.empty()) { if (!meta.series.empty()) {
std::string seriesStr = series; std::string seriesStr = meta.series;
if (!seriesIndex.empty()) { if (!meta.seriesIndex.empty()) {
seriesStr += " #" + seriesIndex; seriesStr += " #" + meta.seriesIndex;
} }
addField(tr(STR_SERIES), seriesStr, false, EpdFontFamily::REGULAR); addField(tr(STR_SERIES), seriesStr, false, EpdFontFamily::REGULAR);
} }
addField(tr(STR_PUBLISHER), publisher, false, EpdFontFamily::REGULAR); addField(tr(STR_PUBLISHER), meta.publisher, false, EpdFontFamily::REGULAR);
addField(tr(STR_DATE), date, false, EpdFontFamily::REGULAR); addField(tr(STR_DATE), meta.date, false, EpdFontFamily::REGULAR);
addField(tr(STR_SUBJECTS), subjects, false, EpdFontFamily::REGULAR); addField(tr(STR_SUBJECTS), meta.subjects, false, EpdFontFamily::REGULAR);
if (!rating.empty()) { if (!meta.rating.empty()) {
int ratingVal = atoi(rating.c_str()); int ratingVal = atoi(meta.rating.c_str());
if (ratingVal > 0 && ratingVal <= 10) { if (ratingVal > 0 && ratingVal <= 10) {
char ratingBuf[8]; char ratingBuf[8];
snprintf(ratingBuf, sizeof(ratingBuf), "%d / 5", (ratingVal + 1) / 2); 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_LANGUAGE), meta.language, false, EpdFontFamily::REGULAR);
addField(tr(STR_ISBN), identifier, false, EpdFontFamily::REGULAR); addField(tr(STR_ISBN), meta.identifier, false, EpdFontFamily::REGULAR);
addField(tr(STR_CONTRIBUTOR), contributor, false, EpdFontFamily::REGULAR); addField(tr(STR_CONTRIBUTOR), meta.contributor, false, EpdFontFamily::REGULAR);
if (fileSize > 0) { if (fileSize > 0) {
addField(tr(STR_FILE_SIZE), formatFileSize(fileSize), false, EpdFontFamily::REGULAR); addField(tr(STR_FILE_SIZE), formatFileSize(fileSize), false, EpdFontFamily::REGULAR);
} }
addField(tr(STR_RIGHTS), rights, false, EpdFontFamily::REGULAR); addField(tr(STR_RIGHTS), meta.rights, false, EpdFontFamily::REGULAR);
addField(tr(STR_DESCRIPTION), description, false, EpdFontFamily::REGULAR); addField(tr(STR_DESCRIPTION), meta.description, false, EpdFontFamily::REGULAR);
const int lineH10 = renderer.getLineHeight(UI_10_FONT_ID); const int lineH10 = renderer.getLineHeight(UI_10_FONT_ID);
const int lineH12 = renderer.getLineHeight(UI_12_FONT_ID); const int lineH12 = renderer.getLineHeight(UI_12_FONT_ID);

View File

@@ -3,6 +3,8 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include <Epub/BookMetadataCache.h>
#include "../Activity.h" #include "../Activity.h"
class BookInfoActivity final : public Activity { class BookInfoActivity final : public Activity {
@@ -31,10 +33,6 @@ class BookInfoActivity final : public Activity {
int coverDisplayHeight = 0; int coverDisplayHeight = 0;
int coverDisplayWidth = 0; int coverDisplayWidth = 0;
void buildLayout(const std::string& title, const std::string& author, const std::string& series, void buildLayout(const BookMetadataCache::BookMetadata& meta, size_t fileSize);
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);
static std::string formatFileSize(size_t bytes); static std::string formatFileSize(size_t bytes);
}; };