diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index cb0b1801..28ab94f9 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -77,6 +77,9 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) { bookMetadata.author = opfParser.author; bookMetadata.language = opfParser.language; bookMetadata.coverItemHref = opfParser.coverItemHref; + bookMetadata.series = opfParser.series; + bookMetadata.seriesIndex = opfParser.seriesIndex; + bookMetadata.description = opfParser.description; // Guide-based cover fallback: if no cover found via metadata/properties, // try extracting the image reference from the guide's cover page XHTML @@ -516,6 +519,30 @@ const std::string& Epub::getLanguage() const { return bookMetadataCache->coreMetadata.language; } +const std::string& Epub::getSeries() const { + static std::string blank; + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { + return blank; + } + return bookMetadataCache->coreMetadata.series; +} + +const std::string& Epub::getSeriesIndex() const { + static std::string blank; + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { + return blank; + } + return bookMetadataCache->coreMetadata.seriesIndex; +} + +const std::string& Epub::getDescription() const { + static std::string blank; + if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { + return blank; + } + return bookMetadataCache->coreMetadata.description; +} + 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 9ffa8d37..768d9458 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -51,6 +51,9 @@ class Epub { const std::string& getTitle() const; const std::string& getAuthor() const; const std::string& getLanguage() const; + const std::string& getSeries() const; + const std::string& getSeriesIndex() const; + const std::string& getDescription() 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 3cdee0b0..efab7bac 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 = 5; +constexpr uint8_t BOOK_CACHE_VERSION = 7; constexpr char bookBinFile[] = "/book.bin"; constexpr char tmpSpineBinFile[] = "/spine.bin.tmp"; constexpr char tmpTocBinFile[] = "/toc.bin.tmp"; @@ -117,7 +117,8 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount); const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() + metadata.coverItemHref.size() + metadata.textReferenceHref.size() + - sizeof(uint32_t) * 5; + metadata.series.size() + metadata.seriesIndex.size() + metadata.description.size() + + sizeof(uint32_t) * 8; const uint32_t lutSize = sizeof(uint32_t) * spineCount + sizeof(uint32_t) * tocCount; const uint32_t lutOffset = headerASize + metadataSize; @@ -132,6 +133,9 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta serialization::writeString(bookFile, metadata.language); serialization::writeString(bookFile, metadata.coverItemHref); serialization::writeString(bookFile, metadata.textReferenceHref); + serialization::writeString(bookFile, metadata.series); + serialization::writeString(bookFile, metadata.seriesIndex); + serialization::writeString(bookFile, metadata.description); // Loop through spine entries, writing LUT positions spineFile.seek(0); @@ -386,6 +390,9 @@ bool BookMetadataCache::load() { serialization::readString(bookFile, coreMetadata.language); serialization::readString(bookFile, coreMetadata.coverItemHref); serialization::readString(bookFile, coreMetadata.textReferenceHref); + serialization::readString(bookFile, coreMetadata.series); + serialization::readString(bookFile, coreMetadata.seriesIndex); + serialization::readString(bookFile, coreMetadata.description); loaded = true; LOG_DBG("BMC", "Loaded cache data: %d spine, %d TOC entries", spineCount, tocCount); diff --git a/lib/Epub/Epub/BookMetadataCache.h b/lib/Epub/Epub/BookMetadataCache.h index 9439b37f..330c29cd 100644 --- a/lib/Epub/Epub/BookMetadataCache.h +++ b/lib/Epub/Epub/BookMetadataCache.h @@ -14,6 +14,9 @@ class BookMetadataCache { std::string language; std::string coverItemHref; std::string textReferenceHref; + std::string series; + std::string seriesIndex; + std::string description; }; struct SpineEntry { diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.cpp b/lib/Epub/Epub/parsers/ContentOpfParser.cpp index 33518bc6..8ca9fc2c 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.cpp +++ b/lib/Epub/Epub/parsers/ContentOpfParser.cpp @@ -10,6 +10,63 @@ namespace { constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml"; constexpr char MEDIA_TYPE_CSS[] = "text/css"; constexpr char itemCacheFile[] = "/.items.bin"; + +// Strip HTML tags and collapse whitespace from a description string. +// Expat already decodes XML entities (< → <), so we see raw angle brackets. +std::string stripHtml(const std::string& html) { + std::string result; + result.reserve(html.size()); + bool inTag = false; + for (size_t i = 0; i < html.size(); ++i) { + const char c = html[i]; + if (c == '<') { + inTag = true; + // Ensure words don't merge when a tag is removed + if (!result.empty() && result.back() != ' ') result += ' '; + } else if (c == '>') { + inTag = false; + } else if (!inTag) { + if (c == '&') { + // Decode common HTML entities not covered by Expat + if (html.compare(i, 6, " ") == 0) { + result += ' '; + i += 5; + } else if (html.compare(i, 7, "–") == 0) { + result += '-'; + i += 6; + } else if (html.compare(i, 7, "—") == 0) { + result += '-'; + i += 6; + } else if (html.compare(i, 8, "…") == 0) { + result += "..."; + i += 7; + } else + result += c; + } else if (c == '\n' || c == '\r' || c == '\t') { + if (!result.empty() && result.back() != ' ') result += ' '; + } else { + result += c; + } + } + } + // Collapse consecutive spaces and trim trailing whitespace + std::string out; + out.reserve(result.size()); + bool lastSpace = false; + for (char c : result) { + if (c == ' ') { + if (!lastSpace && !out.empty()) { + out += ' '; + lastSpace = true; + } + } else { + out += c; + lastSpace = false; + } + } + while (!out.empty() && out.back() == ' ') out.pop_back(); + return out; +} } // namespace bool ContentOpfParser::setup() { @@ -117,6 +174,11 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name return; } + if (self->state == IN_METADATA && strcmp(name, "dc:description") == 0) { + self->state = IN_BOOK_DESCRIPTION; + 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)) { @@ -153,19 +215,25 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name } if (self->state == IN_METADATA && (strcmp(name, "meta") == 0 || strcmp(name, "opf:meta") == 0)) { - bool isCover = false; - std::string coverItemId; + const char* metaName = nullptr; + const char* metaContent = nullptr; for (int i = 0; atts[i]; i += 2) { - if (strcmp(atts[i], "name") == 0 && strcmp(atts[i + 1], "cover") == 0) { - isCover = true; + if (strcmp(atts[i], "name") == 0) { + metaName = atts[i + 1]; } else if (strcmp(atts[i], "content") == 0) { - coverItemId = atts[i + 1]; + metaContent = atts[i + 1]; } } - if (isCover) { - self->coverItemId = coverItemId; + if (metaName && metaContent) { + if (strcmp(metaName, "cover") == 0) { + self->coverItemId = metaContent; + } else if (strcmp(metaName, "calibre:series") == 0 && self->series.empty()) { + self->series = metaContent; + } else if (strcmp(metaName, "calibre:series_index") == 0 && self->seriesIndex.empty()) { + self->seriesIndex = metaContent; + } } return; } @@ -338,6 +406,11 @@ void XMLCALL ContentOpfParser::characterData(void* userData, const XML_Char* s, self->language.append(s, len); return; } + + if (self->state == IN_BOOK_DESCRIPTION) { + self->description.append(s, len); + return; + } } void XMLCALL ContentOpfParser::endElement(void* userData, const XML_Char* name) { @@ -377,6 +450,12 @@ void XMLCALL ContentOpfParser::endElement(void* userData, const XML_Char* name) return; } + if (self->state == IN_BOOK_DESCRIPTION && strcmp(name, "dc:description") == 0) { + self->description = stripHtml(self->description); + 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 89fb3379..24666786 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.h +++ b/lib/Epub/Epub/parsers/ContentOpfParser.h @@ -17,6 +17,7 @@ class ContentOpfParser final : public Print { IN_BOOK_TITLE, IN_BOOK_AUTHOR, IN_BOOK_LANGUAGE, + IN_BOOK_DESCRIPTION, IN_MANIFEST, IN_SPINE, IN_GUIDE, @@ -60,6 +61,9 @@ class ContentOpfParser final : public Print { std::string title; std::string author; std::string language; + std::string description; + std::string series; + std::string seriesIndex; std::string tocNcxPath; std::string tocNavPath; // EPUB 3 nav document path std::string coverItemHref;