Files
Crosspoint/lib/OpdsParser/OpdsParser.cpp
T
jpirnayandArthur Tazhitdinov a7aa4c55d8 fix: Prefer epub format over derived formats when downloading from opds server (#1480)
## Summary

* **What is the goal of this PR?** Prefer epub format over kepub or
other formats offered from an OPDS server
* **What changes are included?**

## Additional Context

Should address #1419 

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**<  NO >**_

---------

Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
2026-05-20 11:46:11 -05:00

185 lines
6.3 KiB
C++

#include "OpdsParser.h"
#include <Logging.h>
#include <XmlParserUtils.h>
#include <cstring>
OpdsParser::OpdsParser() {
parser = XML_ParserCreate(nullptr);
if (!parser) {
errorOccured = true;
LOG_DBG("OPDS", "Couldn't allocate memory for parser");
}
}
OpdsParser::~OpdsParser() { destroyXmlParser(parser); }
size_t OpdsParser::write(uint8_t c) { return write(&c, 1); }
size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
if (errorOccured) return length;
XML_SetUserData(parser, this);
XML_SetElementHandler(parser, startElement, endElement);
XML_SetCharacterDataHandler(parser, characterData);
const char* currentPos = reinterpret_cast<const char*>(xmlData);
size_t remaining = length;
constexpr size_t chunkSize = 1024;
while (remaining > 0) {
void* const buf = XML_GetBuffer(parser, chunkSize);
if (!buf) {
errorOccured = true;
LOG_DBG("OPDS", "Couldn't allocate memory for buffer");
destroyXmlParser(parser);
return length;
}
const size_t toRead = remaining < chunkSize ? remaining : chunkSize;
memcpy(buf, currentPos, toRead);
if (XML_ParseBuffer(parser, static_cast<int>(toRead), 0) == XML_STATUS_ERROR) {
errorOccured = true;
LOG_DBG("OPDS", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
XML_ErrorString(XML_GetErrorCode(parser)));
destroyXmlParser(parser);
return length;
}
currentPos += toRead;
remaining -= toRead;
}
return length;
}
void OpdsParser::flush() {
if (XML_Parse(parser, nullptr, 0, XML_TRUE) != XML_STATUS_OK) {
errorOccured = true;
destroyXmlParser(parser);
}
}
bool OpdsParser::error() const { return errorOccured; }
void OpdsParser::clear() {
entries.clear();
searchTemplate.clear();
nextPageUrl.clear();
prevPageUrl.clear();
currentEntry = OpdsEntry{};
currentText.clear();
inEntry = inTitle = inAuthor = inAuthorName = inId = false;
}
std::vector<OpdsEntry> OpdsParser::getBooks() const {
std::vector<OpdsEntry> books;
for (const auto& entry : entries) {
if (entry.type == OpdsEntryType::BOOK) books.push_back(entry);
}
return books;
}
const char* OpdsParser::findAttribute(const XML_Char** atts, const char* name) {
for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], name) == 0) return atts[i + 1];
}
return nullptr;
}
void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
auto* self = static_cast<OpdsParser*>(userData);
if (strcmp(name, "link") == 0 || strstr(name, ":link") != nullptr) {
const char* href = findAttribute(atts, "href");
if (href) {
const char* rel = findAttribute(atts, "rel");
const char* type = findAttribute(atts, "type");
if (rel && strcmp(rel, "search") == 0) {
std::string sHref(href);
if (sHref.find("{searchTerms}") != std::string::npos) {
self->searchTemplate = sHref;
}
} else if (rel && strcmp(rel, "next") == 0 && !self->inEntry) {
self->nextPageUrl = href;
} else if (rel && strcmp(rel, "previous") == 0 && !self->inEntry) {
self->prevPageUrl = href;
}
if (self->inEntry) {
if (rel && type && strstr(rel, "opds-spec.org/acquisition") != nullptr &&
strcmp(type, "application/epub+zip") == 0) {
// Prefer plain EPUB links over derived formats when multiple
// acquisition links are present for one entry.
const bool isPlainEpub = strstr(href, ".epub") != nullptr || strstr(href, "/epub/") != nullptr;
const bool alreadyHasPlainEpub = self->currentEntry.type == OpdsEntryType::BOOK &&
(self->currentEntry.href.find(".epub") != std::string::npos ||
self->currentEntry.href.find("/epub/") != std::string::npos);
if (self->currentEntry.type != OpdsEntryType::BOOK || (isPlainEpub && !alreadyHasPlainEpub)) {
self->currentEntry.type = OpdsEntryType::BOOK;
self->currentEntry.href = href;
}
} else if (type && strstr(type, "application/atom+xml") != nullptr) {
if (self->currentEntry.type != OpdsEntryType::BOOK) {
self->currentEntry.type = OpdsEntryType::NAVIGATION;
self->currentEntry.href = href;
}
}
}
}
}
if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) {
self->inEntry = true;
self->currentEntry = OpdsEntry{};
return;
}
if (!self->inEntry) return;
if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) {
self->inTitle = true;
self->currentText.clear();
} else if (strcmp(name, "author") == 0 || strstr(name, ":author") != nullptr) {
self->inAuthor = true;
} else if (self->inAuthor && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) {
self->inAuthorName = true;
self->currentText.clear();
} else if (strcmp(name, "id") == 0 || strstr(name, ":id") != nullptr) {
self->inId = true;
self->currentText.clear();
}
}
void XMLCALL OpdsParser::endElement(void* userData, const XML_Char* name) {
auto* self = static_cast<OpdsParser*>(userData);
if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) {
if (!self->currentEntry.title.empty() && !self->currentEntry.href.empty()) {
self->entries.push_back(self->currentEntry);
}
self->inEntry = false;
} else if (self->inEntry) {
if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) {
if (self->inTitle) self->currentEntry.title = self->currentText;
self->inTitle = false;
} else if (strcmp(name, "author") == 0 || strstr(name, ":author") != nullptr) {
self->inAuthor = false;
} else if (self->inAuthorName && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) {
self->currentEntry.author = self->currentText;
self->inAuthorName = false;
} else if (strcmp(name, "id") == 0 || strstr(name, ":id") != nullptr) {
if (self->inId) self->currentEntry.id = self->currentText;
self->inId = false;
}
}
}
void XMLCALL OpdsParser::characterData(void* userData, const XML_Char* s, const int len) {
auto* self = static_cast<OpdsParser*>(userData);
if (self->inTitle || self->inAuthorName || self->inId) {
self->currentText.append(s, len);
}
}