220 lines
6.1 KiB
C++
220 lines
6.1 KiB
C++
|
|
#include "OpdsParser.h"
|
||
|
|
|
||
|
|
#include <HardwareSerial.h>
|
||
|
|
|
||
|
|
#include <cstring>
|
||
|
|
|
||
|
|
OpdsParser::~OpdsParser() {
|
||
|
|
if (parser) {
|
||
|
|
XML_StopParser(parser, XML_FALSE);
|
||
|
|
XML_SetElementHandler(parser, nullptr, nullptr);
|
||
|
|
XML_SetCharacterDataHandler(parser, nullptr);
|
||
|
|
XML_ParserFree(parser);
|
||
|
|
parser = nullptr;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool OpdsParser::parse(const char* xmlData, const size_t length) {
|
||
|
|
clear();
|
||
|
|
|
||
|
|
parser = XML_ParserCreate(nullptr);
|
||
|
|
if (!parser) {
|
||
|
|
Serial.printf("[%lu] [OPDS] Couldn't allocate memory for parser\n", millis());
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
XML_SetUserData(parser, this);
|
||
|
|
XML_SetElementHandler(parser, startElement, endElement);
|
||
|
|
XML_SetCharacterDataHandler(parser, characterData);
|
||
|
|
|
||
|
|
// Parse in chunks to avoid large buffer allocations
|
||
|
|
const char* currentPos = xmlData;
|
||
|
|
size_t remaining = length;
|
||
|
|
constexpr size_t chunkSize = 1024;
|
||
|
|
|
||
|
|
while (remaining > 0) {
|
||
|
|
void* const buf = XML_GetBuffer(parser, chunkSize);
|
||
|
|
if (!buf) {
|
||
|
|
Serial.printf("[%lu] [OPDS] Couldn't allocate memory for buffer\n", millis());
|
||
|
|
XML_ParserFree(parser);
|
||
|
|
parser = nullptr;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
const size_t toRead = remaining < chunkSize ? remaining : chunkSize;
|
||
|
|
memcpy(buf, currentPos, toRead);
|
||
|
|
|
||
|
|
const bool isFinal = (remaining == toRead);
|
||
|
|
if (XML_ParseBuffer(parser, static_cast<int>(toRead), isFinal) == XML_STATUS_ERROR) {
|
||
|
|
Serial.printf("[%lu] [OPDS] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser),
|
||
|
|
XML_ErrorString(XML_GetErrorCode(parser)));
|
||
|
|
XML_ParserFree(parser);
|
||
|
|
parser = nullptr;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
currentPos += toRead;
|
||
|
|
remaining -= toRead;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clean up parser
|
||
|
|
XML_ParserFree(parser);
|
||
|
|
parser = nullptr;
|
||
|
|
|
||
|
|
Serial.printf("[%lu] [OPDS] Parsed %zu entries\n", millis(), entries.size());
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
void OpdsParser::clear() {
|
||
|
|
entries.clear();
|
||
|
|
currentEntry = OpdsEntry{};
|
||
|
|
currentText.clear();
|
||
|
|
inEntry = false;
|
||
|
|
inTitle = false;
|
||
|
|
inAuthor = false;
|
||
|
|
inAuthorName = false;
|
||
|
|
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);
|
||
|
|
|
||
|
|
// Check for entry element (with or without namespace prefix)
|
||
|
|
if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) {
|
||
|
|
self->inEntry = true;
|
||
|
|
self->currentEntry = OpdsEntry{};
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!self->inEntry) return;
|
||
|
|
|
||
|
|
// Check for title element
|
||
|
|
if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) {
|
||
|
|
self->inTitle = true;
|
||
|
|
self->currentText.clear();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for author element
|
||
|
|
if (strcmp(name, "author") == 0 || strstr(name, ":author") != nullptr) {
|
||
|
|
self->inAuthor = true;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for author name element
|
||
|
|
if (self->inAuthor && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) {
|
||
|
|
self->inAuthorName = true;
|
||
|
|
self->currentText.clear();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for id element
|
||
|
|
if (strcmp(name, "id") == 0 || strstr(name, ":id") != nullptr) {
|
||
|
|
self->inId = true;
|
||
|
|
self->currentText.clear();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for link element
|
||
|
|
if (strcmp(name, "link") == 0 || strstr(name, ":link") != nullptr) {
|
||
|
|
const char* rel = findAttribute(atts, "rel");
|
||
|
|
const char* type = findAttribute(atts, "type");
|
||
|
|
const char* href = findAttribute(atts, "href");
|
||
|
|
|
||
|
|
if (href) {
|
||
|
|
// Check for acquisition link with epub type (this is a downloadable book)
|
||
|
|
if (rel && type && strstr(rel, "opds-spec.org/acquisition") != nullptr &&
|
||
|
|
strcmp(type, "application/epub+zip") == 0) {
|
||
|
|
self->currentEntry.type = OpdsEntryType::BOOK;
|
||
|
|
self->currentEntry.href = href;
|
||
|
|
}
|
||
|
|
// Check for navigation link (subsection or no rel specified with atom+xml type)
|
||
|
|
else if (type && strstr(type, "application/atom+xml") != nullptr) {
|
||
|
|
// Only set navigation link if we don't already have an epub link
|
||
|
|
if (self->currentEntry.type != OpdsEntryType::BOOK) {
|
||
|
|
self->currentEntry.type = OpdsEntryType::NAVIGATION;
|
||
|
|
self->currentEntry.href = href;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void XMLCALL OpdsParser::endElement(void* userData, const XML_Char* name) {
|
||
|
|
auto* self = static_cast<OpdsParser*>(userData);
|
||
|
|
|
||
|
|
// Check for entry end
|
||
|
|
if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) {
|
||
|
|
// Only add entry if it has required fields (title and href)
|
||
|
|
if (!self->currentEntry.title.empty() && !self->currentEntry.href.empty()) {
|
||
|
|
self->entries.push_back(self->currentEntry);
|
||
|
|
}
|
||
|
|
self->inEntry = false;
|
||
|
|
self->currentEntry = OpdsEntry{};
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!self->inEntry) return;
|
||
|
|
|
||
|
|
// Check for title end
|
||
|
|
if (strcmp(name, "title") == 0 || strstr(name, ":title") != nullptr) {
|
||
|
|
if (self->inTitle) {
|
||
|
|
self->currentEntry.title = self->currentText;
|
||
|
|
}
|
||
|
|
self->inTitle = false;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for author end
|
||
|
|
if (strcmp(name, "author") == 0 || strstr(name, ":author") != nullptr) {
|
||
|
|
self->inAuthor = false;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for author name end
|
||
|
|
if (self->inAuthor && (strcmp(name, "name") == 0 || strstr(name, ":name") != nullptr)) {
|
||
|
|
if (self->inAuthorName) {
|
||
|
|
self->currentEntry.author = self->currentText;
|
||
|
|
}
|
||
|
|
self->inAuthorName = false;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check for id end
|
||
|
|
if (strcmp(name, "id") == 0 || strstr(name, ":id") != nullptr) {
|
||
|
|
if (self->inId) {
|
||
|
|
self->currentEntry.id = self->currentText;
|
||
|
|
}
|
||
|
|
self->inId = false;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void XMLCALL OpdsParser::characterData(void* userData, const XML_Char* s, const int len) {
|
||
|
|
auto* self = static_cast<OpdsParser*>(userData);
|
||
|
|
|
||
|
|
// Only accumulate text when in a text element
|
||
|
|
if (self->inTitle || self->inAuthorName || self->inId) {
|
||
|
|
self->currentText.append(s, len);
|
||
|
|
}
|
||
|
|
}
|