Extend metadata
This commit is contained in:
@@ -77,6 +77,9 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
|||||||
bookMetadata.author = opfParser.author;
|
bookMetadata.author = opfParser.author;
|
||||||
bookMetadata.language = opfParser.language;
|
bookMetadata.language = opfParser.language;
|
||||||
bookMetadata.coverItemHref = opfParser.coverItemHref;
|
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,
|
// Guide-based cover fallback: if no cover found via metadata/properties,
|
||||||
// try extracting the image reference from the guide's cover page XHTML
|
// 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;
|
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 {
|
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";
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ class Epub {
|
|||||||
const std::string& getTitle() const;
|
const std::string& getTitle() const;
|
||||||
const std::string& getAuthor() const;
|
const std::string& getAuthor() const;
|
||||||
const std::string& getLanguage() 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;
|
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;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
#include "FsHelpers.h"
|
#include "FsHelpers.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t BOOK_CACHE_VERSION = 5;
|
constexpr uint8_t BOOK_CACHE_VERSION = 7;
|
||||||
constexpr char bookBinFile[] = "/book.bin";
|
constexpr char bookBinFile[] = "/book.bin";
|
||||||
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
||||||
constexpr char tmpTocBinFile[] = "/toc.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);
|
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() +
|
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() +
|
||||||
metadata.coverItemHref.size() + metadata.textReferenceHref.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 lutSize = sizeof(uint32_t) * spineCount + sizeof(uint32_t) * tocCount;
|
||||||
const uint32_t lutOffset = headerASize + metadataSize;
|
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.language);
|
||||||
serialization::writeString(bookFile, metadata.coverItemHref);
|
serialization::writeString(bookFile, metadata.coverItemHref);
|
||||||
serialization::writeString(bookFile, metadata.textReferenceHref);
|
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
|
// Loop through spine entries, writing LUT positions
|
||||||
spineFile.seek(0);
|
spineFile.seek(0);
|
||||||
@@ -386,6 +390,9 @@ bool BookMetadataCache::load() {
|
|||||||
serialization::readString(bookFile, coreMetadata.language);
|
serialization::readString(bookFile, coreMetadata.language);
|
||||||
serialization::readString(bookFile, coreMetadata.coverItemHref);
|
serialization::readString(bookFile, coreMetadata.coverItemHref);
|
||||||
serialization::readString(bookFile, coreMetadata.textReferenceHref);
|
serialization::readString(bookFile, coreMetadata.textReferenceHref);
|
||||||
|
serialization::readString(bookFile, coreMetadata.series);
|
||||||
|
serialization::readString(bookFile, coreMetadata.seriesIndex);
|
||||||
|
serialization::readString(bookFile, coreMetadata.description);
|
||||||
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
LOG_DBG("BMC", "Loaded cache data: %d spine, %d TOC entries", spineCount, tocCount);
|
LOG_DBG("BMC", "Loaded cache data: %d spine, %d TOC entries", spineCount, tocCount);
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ class BookMetadataCache {
|
|||||||
std::string language;
|
std::string language;
|
||||||
std::string coverItemHref;
|
std::string coverItemHref;
|
||||||
std::string textReferenceHref;
|
std::string textReferenceHref;
|
||||||
|
std::string series;
|
||||||
|
std::string seriesIndex;
|
||||||
|
std::string description;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SpineEntry {
|
struct SpineEntry {
|
||||||
|
|||||||
@@ -10,6 +10,63 @@ namespace {
|
|||||||
constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
|
constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
|
||||||
constexpr char MEDIA_TYPE_CSS[] = "text/css";
|
constexpr char MEDIA_TYPE_CSS[] = "text/css";
|
||||||
constexpr char itemCacheFile[] = "/.items.bin";
|
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
|
} // namespace
|
||||||
|
|
||||||
bool ContentOpfParser::setup() {
|
bool ContentOpfParser::setup() {
|
||||||
@@ -117,6 +174,11 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
return;
|
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)) {
|
if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
|
||||||
self->state = IN_MANIFEST;
|
self->state = IN_MANIFEST;
|
||||||
if (!Storage.openFileForWrite("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
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)) {
|
if (self->state == IN_METADATA && (strcmp(name, "meta") == 0 || strcmp(name, "opf:meta") == 0)) {
|
||||||
bool isCover = false;
|
const char* metaName = nullptr;
|
||||||
std::string coverItemId;
|
const char* metaContent = nullptr;
|
||||||
|
|
||||||
for (int i = 0; atts[i]; i += 2) {
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
if (strcmp(atts[i], "name") == 0 && strcmp(atts[i + 1], "cover") == 0) {
|
if (strcmp(atts[i], "name") == 0) {
|
||||||
isCover = true;
|
metaName = atts[i + 1];
|
||||||
} else if (strcmp(atts[i], "content") == 0) {
|
} else if (strcmp(atts[i], "content") == 0) {
|
||||||
coverItemId = atts[i + 1];
|
metaContent = atts[i + 1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCover) {
|
if (metaName && metaContent) {
|
||||||
self->coverItemId = coverItemId;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -338,6 +406,11 @@ void XMLCALL ContentOpfParser::characterData(void* userData, const XML_Char* s,
|
|||||||
self->language.append(s, len);
|
self->language.append(s, len);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_BOOK_DESCRIPTION) {
|
||||||
|
self->description.append(s, len);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void XMLCALL ContentOpfParser::endElement(void* userData, const XML_Char* name) {
|
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;
|
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)) {
|
if (self->state == IN_METADATA && (strcmp(name, "metadata") == 0 || strcmp(name, "opf:metadata") == 0)) {
|
||||||
self->state = IN_PACKAGE;
|
self->state = IN_PACKAGE;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class ContentOpfParser final : public Print {
|
|||||||
IN_BOOK_TITLE,
|
IN_BOOK_TITLE,
|
||||||
IN_BOOK_AUTHOR,
|
IN_BOOK_AUTHOR,
|
||||||
IN_BOOK_LANGUAGE,
|
IN_BOOK_LANGUAGE,
|
||||||
|
IN_BOOK_DESCRIPTION,
|
||||||
IN_MANIFEST,
|
IN_MANIFEST,
|
||||||
IN_SPINE,
|
IN_SPINE,
|
||||||
IN_GUIDE,
|
IN_GUIDE,
|
||||||
@@ -60,6 +61,9 @@ class ContentOpfParser final : public Print {
|
|||||||
std::string title;
|
std::string title;
|
||||||
std::string author;
|
std::string author;
|
||||||
std::string language;
|
std::string language;
|
||||||
|
std::string description;
|
||||||
|
std::string series;
|
||||||
|
std::string seriesIndex;
|
||||||
std::string tocNcxPath;
|
std::string tocNcxPath;
|
||||||
std::string tocNavPath; // EPUB 3 nav document path
|
std::string tocNavPath; // EPUB 3 nav document path
|
||||||
std::string coverItemHref;
|
std::string coverItemHref;
|
||||||
|
|||||||
Reference in New Issue
Block a user