diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index f2f6c067..a2726a7e 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -81,6 +81,13 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) { bookMetadata.series = opfParser.series; bookMetadata.seriesIndex = opfParser.seriesIndex; bookMetadata.description = opfParser.description; + bookMetadata.publisher = opfParser.publisher; + bookMetadata.date = opfParser.date; + bookMetadata.subjects = opfParser.subjects; + bookMetadata.rights = opfParser.rights; + bookMetadata.contributor = opfParser.contributor; + bookMetadata.identifier = opfParser.identifier; + bookMetadata.rating = opfParser.rating; // Guide-based cover fallback: if no cover found via metadata/properties, // try extracting the image reference from the guide's cover page XHTML @@ -547,6 +554,48 @@ const std::string& Epub::getDescription() const { return bookMetadataCache->coreMetadata.description; } +const std::string& Epub::getPublisher() const { + static std::string blank; + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank; + return bookMetadataCache->coreMetadata.publisher; +} + +const std::string& Epub::getDate() const { + static std::string blank; + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank; + return bookMetadataCache->coreMetadata.date; +} + +const std::string& Epub::getSubjects() const { + static std::string blank; + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank; + return bookMetadataCache->coreMetadata.subjects; +} + +const std::string& Epub::getRights() const { + static std::string blank; + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank; + return bookMetadataCache->coreMetadata.rights; +} + +const std::string& Epub::getContributor() const { + static std::string blank; + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank; + return bookMetadataCache->coreMetadata.contributor; +} + +const std::string& Epub::getIdentifier() const { + static std::string blank; + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank; + return bookMetadataCache->coreMetadata.identifier; +} + +const std::string& Epub::getRating() const { + static std::string blank; + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return blank; + return bookMetadataCache->coreMetadata.rating; +} + 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 6a412857..4acbad1e 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -54,6 +54,13 @@ class Epub { const std::string& getSeries() const; const std::string& getSeriesIndex() const; const std::string& getDescription() const; + const std::string& getPublisher() const; + const std::string& getDate() const; + const std::string& getSubjects() const; + const std::string& getRights() const; + const std::string& getContributor() const; + const std::string& getIdentifier() const; + const std::string& getRating() const; std::string getCoverBmpPath(bool cropped = false) const; bool generateCoverBmp(bool cropped = false) const; std::string getThumbBmpPath() const; diff --git a/lib/Epub/Epub/BookMetadataCache.cpp b/lib/Epub/Epub/BookMetadataCache.cpp index 6fb3777c..cdc8e834 100644 --- a/lib/Epub/Epub/BookMetadataCache.cpp +++ b/lib/Epub/Epub/BookMetadataCache.cpp @@ -9,7 +9,7 @@ #include "FsHelpers.h" namespace { -constexpr uint8_t BOOK_CACHE_VERSION = 6; +constexpr uint8_t BOOK_CACHE_VERSION = 7; constexpr char bookBinFile[] = "/book.bin"; constexpr char tmpSpineBinFile[] = "/spine.bin.tmp"; constexpr char tmpTocBinFile[] = "/toc.bin.tmp"; @@ -118,7 +118,9 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() + metadata.coverItemHref.size() + metadata.textReferenceHref.size() + metadata.series.size() + metadata.seriesIndex.size() + metadata.description.size() + - sizeof(uint32_t) * 8; + metadata.publisher.size() + metadata.date.size() + metadata.subjects.size() + + metadata.rights.size() + metadata.contributor.size() + metadata.identifier.size() + + metadata.rating.size() + sizeof(uint32_t) * 15; const uint32_t lutSize = sizeof(uint32_t) * spineCount + sizeof(uint32_t) * tocCount; const uint32_t lutOffset = headerASize + metadataSize; @@ -136,6 +138,13 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta serialization::writeString(bookFile, metadata.series); serialization::writeString(bookFile, metadata.seriesIndex); serialization::writeString(bookFile, metadata.description); + serialization::writeString(bookFile, metadata.publisher); + serialization::writeString(bookFile, metadata.date); + serialization::writeString(bookFile, metadata.subjects); + serialization::writeString(bookFile, metadata.rights); + serialization::writeString(bookFile, metadata.contributor); + serialization::writeString(bookFile, metadata.identifier); + serialization::writeString(bookFile, metadata.rating); // Loop through spine entries, writing LUT positions spineFile.seek(0); @@ -392,7 +401,14 @@ bool BookMetadataCache::load() { !serialization::readString(bookFile, coreMetadata.textReferenceHref) || !serialization::readString(bookFile, coreMetadata.series) || !serialization::readString(bookFile, coreMetadata.seriesIndex) || - !serialization::readString(bookFile, coreMetadata.description)) { + !serialization::readString(bookFile, coreMetadata.description) || + !serialization::readString(bookFile, coreMetadata.publisher) || + !serialization::readString(bookFile, coreMetadata.date) || + !serialization::readString(bookFile, coreMetadata.subjects) || + !serialization::readString(bookFile, coreMetadata.rights) || + !serialization::readString(bookFile, coreMetadata.contributor) || + !serialization::readString(bookFile, coreMetadata.identifier) || + !serialization::readString(bookFile, coreMetadata.rating)) { LOG_ERR("BMC", "Failed to read metadata strings from cache"); bookFile.close(); return false; diff --git a/lib/Epub/Epub/BookMetadataCache.h b/lib/Epub/Epub/BookMetadataCache.h index 330c29cd..65253f81 100644 --- a/lib/Epub/Epub/BookMetadataCache.h +++ b/lib/Epub/Epub/BookMetadataCache.h @@ -17,6 +17,13 @@ class BookMetadataCache { std::string series; std::string seriesIndex; std::string description; + std::string publisher; + std::string date; + std::string subjects; + std::string rights; + std::string contributor; + std::string identifier; + std::string rating; }; struct SpineEntry { diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.cpp b/lib/Epub/Epub/parsers/ContentOpfParser.cpp index 795d8471..e2a93d88 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.cpp +++ b/lib/Epub/Epub/parsers/ContentOpfParser.cpp @@ -158,6 +158,59 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name return; } + if (self->state == IN_METADATA && strcmp(name, "dc:publisher") == 0) { + if (self->publisher.empty()) { + self->state = IN_BOOK_PUBLISHER; + } + return; + } + + if (self->state == IN_METADATA && strcmp(name, "dc:date") == 0) { + if (self->date.empty()) { + self->state = IN_BOOK_DATE; + } + return; + } + + if (self->state == IN_METADATA && strcmp(name, "dc:subject") == 0) { + if (!self->subjects.empty()) { + self->subjects += ", "; + } + self->state = IN_BOOK_SUBJECT; + return; + } + + if (self->state == IN_METADATA && strcmp(name, "dc:rights") == 0) { + if (self->rights.empty()) { + self->state = IN_BOOK_RIGHTS; + } + return; + } + + if (self->state == IN_METADATA && strcmp(name, "dc:contributor") == 0) { + if (!self->contributor.empty()) { + self->contributor += ", "; + } + self->state = IN_BOOK_CONTRIBUTOR; + return; + } + + if (self->state == IN_METADATA && strcmp(name, "dc:identifier") == 0) { + self->identifierIsIsbn = false; + for (int i = 0; atts[i]; i += 2) { + if (strcmp(atts[i], "opf:scheme") == 0 && strcasecmp(atts[i + 1], "ISBN") == 0) { + self->identifierIsIsbn = true; + } + } + if (self->identifier.empty() || self->identifierIsIsbn) { + if (self->identifierIsIsbn) { + self->identifier.clear(); + } + self->state = IN_BOOK_IDENTIFIER; + } + return; + } + if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) { self->state = IN_MANIFEST; if (!Storage.openFileForWrite("COF", self->cachePath + itemCacheFile, self->tempItemStore)) { @@ -221,6 +274,8 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name self->series = trim(std::string(metaContent, std::min(strlen(metaContent), MAX_DESCRIPTION_LENGTH))); } else if (strcmp(metaName, "calibre:series_index") == 0 && self->seriesIndex.empty()) { self->seriesIndex = trim(std::string(metaContent, std::min(strlen(metaContent), MAX_DESCRIPTION_LENGTH))); + } else if (strcmp(metaName, "calibre:rating") == 0 && self->rating.empty()) { + self->rating = trim(std::string(metaContent, std::min(strlen(metaContent), static_cast(8)))); } } @@ -439,6 +494,42 @@ void XMLCALL ContentOpfParser::characterData(void* userData, const XML_Char* s, } return; } + + if (self->state == IN_BOOK_PUBLISHER) { + self->publisher.append(s, std::min(static_cast(len), MAX_DESCRIPTION_LENGTH - self->publisher.size())); + return; + } + + if (self->state == IN_BOOK_DATE) { + self->date.append(s, std::min(static_cast(len), MAX_DESCRIPTION_LENGTH - self->date.size())); + return; + } + + if (self->state == IN_BOOK_SUBJECT) { + const size_t remaining = MAX_DESCRIPTION_LENGTH - self->subjects.size(); + if (remaining > 0) { + self->subjects.append(s, std::min(static_cast(len), remaining)); + } + return; + } + + if (self->state == IN_BOOK_RIGHTS) { + self->rights.append(s, std::min(static_cast(len), MAX_DESCRIPTION_LENGTH - self->rights.size())); + return; + } + + if (self->state == IN_BOOK_CONTRIBUTOR) { + const size_t remaining = MAX_DESCRIPTION_LENGTH - self->contributor.size(); + if (remaining > 0) { + self->contributor.append(s, std::min(static_cast(len), remaining)); + } + return; + } + + if (self->state == IN_BOOK_IDENTIFIER) { + self->identifier.append(s, std::min(static_cast(len), MAX_DESCRIPTION_LENGTH - self->identifier.size())); + return; + } } void XMLCALL ContentOpfParser::endElement(void* userData, const XML_Char* name) { @@ -496,6 +587,40 @@ void XMLCALL ContentOpfParser::endElement(void* userData, const XML_Char* name) return; } + if (self->state == IN_BOOK_PUBLISHER && strcmp(name, "dc:publisher") == 0) { + self->publisher = trim(self->publisher); + self->state = IN_METADATA; + return; + } + + if (self->state == IN_BOOK_DATE && strcmp(name, "dc:date") == 0) { + self->date = trim(self->date); + self->state = IN_METADATA; + return; + } + + if (self->state == IN_BOOK_SUBJECT && strcmp(name, "dc:subject") == 0) { + self->state = IN_METADATA; + return; + } + + if (self->state == IN_BOOK_RIGHTS && strcmp(name, "dc:rights") == 0) { + self->rights = trim(self->rights); + self->state = IN_METADATA; + return; + } + + if (self->state == IN_BOOK_CONTRIBUTOR && strcmp(name, "dc:contributor") == 0) { + self->state = IN_METADATA; + return; + } + + if (self->state == IN_BOOK_IDENTIFIER && strcmp(name, "dc:identifier") == 0) { + self->identifier = trim(self->identifier); + self->state = IN_METADATA; + return; + } + if (self->state == IN_METADATA && (strcmp(name, "metadata") == 0 || strcmp(name, "opf:metadata") == 0)) { self->state = IN_PACKAGE; return; diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.h b/lib/Epub/Epub/parsers/ContentOpfParser.h index c633f70b..e76db78b 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.h +++ b/lib/Epub/Epub/parsers/ContentOpfParser.h @@ -20,6 +20,12 @@ class ContentOpfParser final : public Print { IN_BOOK_DESCRIPTION, IN_BOOK_SERIES, IN_BOOK_SERIES_INDEX, + IN_BOOK_PUBLISHER, + IN_BOOK_DATE, + IN_BOOK_SUBJECT, + IN_BOOK_RIGHTS, + IN_BOOK_CONTRIBUTOR, + IN_BOOK_IDENTIFIER, IN_MANIFEST, IN_SPINE, IN_GUIDE, @@ -33,6 +39,7 @@ class ContentOpfParser final : public Print { BookMetadataCache* cache; FsFile tempItemStore; std::string coverItemId; + bool identifierIsIsbn = false; // Index for fast idref→href lookup (used only for large EPUBs) struct ItemIndexEntry { @@ -66,6 +73,13 @@ class ContentOpfParser final : public Print { std::string series; std::string seriesIndex; std::string description; + std::string publisher; + std::string date; + std::string subjects; + std::string rights; + std::string contributor; + std::string identifier; + std::string rating; std::string tocNcxPath; std::string tocNavPath; // EPUB 3 nav document path std::string coverItemHref; diff --git a/lib/I18n/I18nKeys.h b/lib/I18n/I18nKeys.h index 017ba191..fb3da842 100644 --- a/lib/I18n/I18nKeys.h +++ b/lib/I18n/I18nKeys.h @@ -452,6 +452,13 @@ enum class StrId : uint16_t { STR_SERIES, STR_FILE_SIZE, STR_DESCRIPTION, + STR_PUBLISHER, + STR_DATE, + STR_SUBJECTS, + STR_RATING, + STR_ISBN, + STR_RIGHTS, + STR_CONTRIBUTOR, STR_MANAGE, STR_INFO, STR_ARCHIVE_BOOK, diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index c95e9448..6dc79e71 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -399,6 +399,13 @@ STR_AUTHOR: "Author" STR_SERIES: "Series" STR_FILE_SIZE: "File Size" STR_DESCRIPTION: "Description" +STR_PUBLISHER: "Publisher" +STR_DATE: "Date" +STR_SUBJECTS: "Subjects" +STR_RATING: "Rating" +STR_ISBN: "ISBN" +STR_RIGHTS: "Rights" +STR_CONTRIBUTOR: "Contributor" STR_MANAGE: "Manage" STR_INFO: "Info" STR_ARCHIVE_BOOK: "Archive Book" diff --git a/src/activities/home/BookInfoActivity.cpp b/src/activities/home/BookInfoActivity.cpp index 410e49f7..a3f8377c 100644 --- a/src/activities/home/BookInfoActivity.cpp +++ b/src/activities/home/BookInfoActivity.cpp @@ -60,6 +60,7 @@ void BookInfoActivity::onEnter() { } std::string title, author, series, seriesIndex, description, language; + std::string publisher, date, subjects, rights, contributor, identifier, rating; if (FsHelpers::hasEpubExtension(fileName)) { Epub epub(filePath, "/.crosspoint"); @@ -78,6 +79,13 @@ void BookInfoActivity::onEnter() { 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; if (epub.generateThumbBmp(coverH)) { @@ -129,7 +137,8 @@ void BookInfoActivity::onEnter() { title = fileName; } - buildLayout(title, author, series, seriesIndex, description, language, fileSize); + buildLayout(title, author, series, seriesIndex, description, language, fileSize, publisher, date, subjects, rights, + contributor, identifier, rating); requestUpdate(); } @@ -137,9 +146,12 @@ 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& 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; - fields.reserve(6); + fields.reserve(13); auto addField = [&](const char* label, const std::string& text, bool bold, EpdFontFamily::Style style) { if (text.empty()) return; @@ -161,12 +173,28 @@ void BookInfoActivity::buildLayout(const std::string& title, const std::string& 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); + + if (!rating.empty()) { + int ratingVal = atoi(rating.c_str()); + if (ratingVal > 0 && ratingVal <= 10) { + char ratingBuf[8]; + snprintf(ratingBuf, sizeof(ratingBuf), "%d / 5", (ratingVal + 1) / 2); + addField(tr(STR_RATING), std::string(ratingBuf), false, EpdFontFamily::REGULAR); + } + } + addField(tr(STR_LANGUAGE), language, false, EpdFontFamily::REGULAR); + addField(tr(STR_ISBN), identifier, false, EpdFontFamily::REGULAR); + addField(tr(STR_CONTRIBUTOR), 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); const int lineH10 = renderer.getLineHeight(UI_10_FONT_ID); diff --git a/src/activities/home/BookInfoActivity.h b/src/activities/home/BookInfoActivity.h index bdb6e78f..9de65c77 100644 --- a/src/activities/home/BookInfoActivity.h +++ b/src/activities/home/BookInfoActivity.h @@ -33,6 +33,8 @@ class BookInfoActivity final : public Activity { 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); + 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); };