feat: add OPDS search support & next/prev page navigation (#1462)

## Summary

**What is the goal of this PR?**
Adds OPDS search support, allowing users to search a catalog directly
from the book browser when the server exposes an OpenSearch template.

**What changes are included?**
- `OpdsParser`: parses the OpenSearch template URL from feed-level
`<link rel="search">` elements and exposes it via `getSearchTemplate()`
- `OpdsBookBrowserActivity`: fetches and stores the search template
after each feed load; shows a Search hint on the Left button when a
template is available; launches the existing `KeyboardEntryActivity` for
query input; URL-encodes the query and fetches the result feed
- Absolute search result URLs are handled correctly in `fetchFeed`
(skips prepending the server base URL)
- A `consumeConfirm` guard prevents the Confirm release that submits the
keyboard from immediately triggering a book download on the first
browsing frame after search results load

## Additional Context

- Search is silently unavailable if the server does not advertise an
OpenSearch template — no UI change in that case
- Tested against a Calibre-Web OPDS endpoint which exposes `<link
rel="search" type="application/opensearchdescription+xml">`
- The inline URL encoder in `performSearch` was necessary as
`StringUtils` has no such utility; worth considering extracting to
`StringUtils` in a follow-up
- No new dependencies introduced

---

### 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: kira <rammah@tuta.io>
Co-authored-by: Justin Mitchell <justin@jmitch.com>
This commit is contained in:
rxmmah
2026-04-12 12:24:27 -04:00
committed by GitHub
co-authored by kira Justin Mitchell
parent 5c12f2f01e
commit fa2a3d2539
25 changed files with 208 additions and 277 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Адкрыйце гэты адрас у браўзеры"
STR_OR_HTTP_PREFIX: "або http://"
STR_SCAN_QR_HINT: "або адсканіруйце QR-код:"
STR_CALIBRE_WIRELESS: "Calibre па Wi-Fi"
STR_CALIBRE_WEB_URL: "Вэб-адрас Calibre"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Абаронена | + = Захавана"
STR_MAC_ADDRESS: "MAC-адрас:"
STR_CHECKING_WIFI: "Праверка Wi-Fi..."
+1 -1
View File
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Obriu aquest URL al navegador"
STR_OR_HTTP_PREFIX: "o http://"
STR_SCAN_QR_HINT: "o escanegeu el codi QR amb el telèfon:"
STR_CALIBRE_WIRELESS: "Calibre sense fils"
STR_CALIBRE_WEB_URL: "URL web del Calibre"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Encriptat | + = Desat"
STR_MAC_ADDRESS: "Adreça MAC:"
STR_CHECKING_WIFI: "S'està comprovant el WiFi..."
+1 -1
View File
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Otevřete tuto URL ve svém prohlížeči"
STR_OR_HTTP_PREFIX: "nebo http://"
STR_SCAN_QR_HINT: "nebo naskenujte QR kód telefonem:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "URL webu Calibre"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Šifrováno | + = Uloženo"
STR_MAC_ADDRESS: "MAC adresa:"
STR_CHECKING_WIFI: "Kontrola WiFi..."
+1 -1
View File
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Åbn denne URL i din browser"
STR_OR_HTTP_PREFIX: "eller http://"
STR_SCAN_QR_HINT: "eller scan QR-kode med din telefon:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "Calibre Web URL"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Krypteret | + = Gemt"
STR_MAC_ADDRESS: "MAC-adresse:"
STR_CHECKING_WIFI: "Tjekker WiFi..."
+1 -1
View File
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Open deze URL in je browser"
STR_OR_HTTP_PREFIX: "of http://"
STR_SCAN_QR_HINT: "of scan de QR-code met je telefoon:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "Calibre Web URL"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Beveiligd | + = Opgeslagen"
STR_MAC_ADDRESS: "MAC-adres:"
STR_CHECKING_WIFI: "Wifi controleren..."
+4 -1
View File
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Open this URL in your browser"
STR_OR_HTTP_PREFIX: "or http://"
STR_SCAN_QR_HINT: "or scan QR code with your phone:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "Calibre Web URL"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Encrypted | + = Saved"
STR_MAC_ADDRESS: "MAC address:"
STR_CHECKING_WIFI: "Checking WiFi..."
@@ -175,6 +175,8 @@ STR_UNNAMED: "Unnamed"
STR_NO_SERVER_URL: "No server URL configured"
STR_FETCH_FEED_FAILED: "Failed to fetch feed"
STR_PARSE_FEED_FAILED: "Failed to parse feed"
STR_NEXT_PAGE: "Next Page »"
STR_PREV_PAGE: "« Previous Page"
STR_NETWORK_PREFIX: "Network: "
STR_IP_ADDRESS_PREFIX: "IP Address: "
STR_ERROR_GENERAL_FAILURE: "Error: General failure"
@@ -229,6 +231,7 @@ STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix"
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
STR_OPDS_BROWSER: "OPDS Browser"
STR_SEARCH: "Search"
STR_COVER_CUSTOM: "Cover + Custom"
STR_MENU_RECENT_BOOKS: "Recent Books"
STR_NO_RECENT_BOOKS: "No recent books"
+1 -1
View File
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Avaa tämä osoite selaimessasi"
STR_OR_HTTP_PREFIX: "tai http://"
STR_SCAN_QR_HINT: "tai skannaa QR-koodi puhelimellasi:"
STR_CALIBRE_WIRELESS: "Calibre langaton"
STR_CALIBRE_WEB_URL: "Calibre-verkko-osoite"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Salattu | + = Tallennettu"
STR_MAC_ADDRESS: "MAC-osoite:"
STR_CHECKING_WIFI: "Tarkistetaan WiFi..."
+1 -1
View File
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Ouvrez cette URL dans un navigateur"
STR_OR_HTTP_PREFIX: "ou http://"
STR_SCAN_QR_HINT: "ou scannez le QR code :"
STR_CALIBRE_WIRELESS: "Connexion Calibre sans fil"
STR_CALIBRE_WEB_URL: "URL Web Calibre"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Sécurisé | + = Sauvegardé"
STR_MAC_ADDRESS: "Adresse MAC :"
STR_CHECKING_WIFI: "Vérification du WiFi…"
+1 -1
View File
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Diese URL im Browser öffnen"
STR_OR_HTTP_PREFIX: "oder http://"
STR_SCAN_QR_HINT: "oder QR-Code mit dem Handy scannen:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "Calibre-Web-URL"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Verschlüsselt | + = Gespeichert"
STR_MAC_ADDRESS: "MAC-Adresse:"
STR_CHECKING_WIFI: "WLAN prüfen…"
+1 -1
View File
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Nyisd meg ezt az URL-t a böngésződben"
STR_OR_HTTP_PREFIX: "vagy http://"
STR_SCAN_QR_HINT: "vagy olvasd be a QR-kódot a telefonoddal:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "Calibre Web URL"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Titkosított | + = Mentett"
STR_MAC_ADDRESS: "MAC-cím:"
STR_CHECKING_WIFI: "WiFi ellenőrzése..."
+1 -1
View File
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Apri questo URL nel tuo browser"
STR_OR_HTTP_PREFIX: "o http://"
STR_SCAN_QR_HINT: "o scansiona il codice QR con il tuo telefono:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "URL Web Calibre"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Criptata | + = Salvata"
STR_MAC_ADDRESS: "Indirizzo MAC:"
STR_CHECKING_WIFI: "Controllo WiFi in corso..."
+1 -1
View File
@@ -44,7 +44,7 @@ STR_OPEN_URL_HINT: "Браузерде осы URL мекенжайын ашың
STR_OR_HTTP_PREFIX: "немесе http://"
STR_SCAN_QR_HINT: "немесе телефонмен QR кодын сканерлеңіз:"
STR_CALIBRE_WIRELESS: "Calibre сымсыз"
STR_CALIBRE_WEB_URL: "Calibre Web URL"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Шифрланған | + = Сақталған"
STR_MAC_ADDRESS: "MAC мекенжайы:"
STR_CHECKING_WIFI: "WiFi тексерілуде..."
+1 -1
View File
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Atidarykite šį adresą naršyklėje"
STR_OR_HTTP_PREFIX: "arba http://"
STR_SCAN_QR_HINT: "arba nuskaitykite QR kodą:"
STR_CALIBRE_WIRELESS: "Calibre belaidis"
STR_CALIBRE_WEB_URL: "Calibre Web URL"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Užšifruota | + = Išsaugota"
STR_MAC_ADDRESS: "MAC adresas:"
STR_CHECKING_WIFI: "Tikrinamas WiFi..."
+1 -1
View File
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Otwórz ten URL w przeglądarce"
STR_OR_HTTP_PREFIX: "albo http://"
STR_SCAN_QR_HINT: "albo zeskanuj kod QR telefonem:"
STR_CALIBRE_WIRELESS: "Bezprzewodowe połączenie z Calibre"
STR_CALIBRE_WEB_URL: "Calibre Web URL"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Zaszyfrowane | + = Zapisane"
STR_MAC_ADDRESS: "Adres MAC:"
STR_CHECKING_WIFI: "Sprawdzanie WiFi..."
+1 -1
View File
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Abra este URL seu navegador"
STR_OR_HTTP_PREFIX: "ou http://"
STR_SCAN_QR_HINT: "ou escaneie o QR code com seu celular:"
STR_CALIBRE_WIRELESS: "Calibre sem fio"
STR_CALIBRE_WEB_URL: "URL do Calibre Web"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Criptografada | + = Salva"
STR_MAC_ADDRESS: "Endereço MAC:"
STR_CHECKING_WIFI: "Verificando WiFi..."
+1 -1
View File
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Deschideţi acest URL în browserul dvs."
STR_OR_HTTP_PREFIX: "sau http://"
STR_SCAN_QR_HINT: "sau scanaţi codul QR cu telefonul dvs.:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_CALIBRE_WEB_URL: "Calibre URL"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Criptat | + = Salvat"
STR_MAC_ADDRESS: "Adresă MAC:"
STR_CHECKING_WIFI: "Verificare WiFi..."
+1 -1
View File
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Откройте этот адрес в браузере"
STR_OR_HTTP_PREFIX: "или http://"
STR_SCAN_QR_HINT: "или отсканируйте QR-код:"
STR_CALIBRE_WIRELESS: "Calibre по Wi-Fi"
STR_CALIBRE_WEB_URL: "Web-адрес Calibre"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Защищена | + = Сохранена"
STR_MAC_ADDRESS: "MAC-адрес:"
STR_CHECKING_WIFI: "Проверка Wi-Fi..."
+1 -1
View File
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Abra esta dirección en su navegador"
STR_OR_HTTP_PREFIX: "o http://"
STR_SCAN_QR_HINT: "o escanee el código QR con su móvil:"
STR_CALIBRE_WIRELESS: "Calibre inalámbrico"
STR_CALIBRE_WEB_URL: "URL del sitio web de Calibre"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* (Cifrado) | + (Guardado)"
STR_MAC_ADDRESS: "MAC Address:"
STR_CHECKING_WIFI: "Verificando Wi-Fi..."
+1 -1
View File
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Öppna denna adress i din browser"
STR_OR_HTTP_PREFIX: "eller http://"
STR_SCAN_QR_HINT: "eller skanna QR-kod med din telefon:"
STR_CALIBRE_WIRELESS: "Calibre Trådlöst"
STR_CALIBRE_WEB_URL: "Calibre webbadress"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Krypterad | + = Sparad"
STR_MAC_ADDRESS: "MAC-adress:"
STR_CHECKING_WIFI: "Kontrollerar trådlöst nätverk…"
+1 -1
View File
@@ -44,7 +44,7 @@ STR_OPEN_URL_HINT: "Tarayıcınızda bu adresi açın"
STR_OR_HTTP_PREFIX: "veya http://"
STR_SCAN_QR_HINT: "veya telefonunuzla QR kodu tarayın:"
STR_CALIBRE_WIRELESS: "Calibre Kablosuz"
STR_CALIBRE_WEB_URL: "Calibre Web Adresi"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Şifreli | + = Kayıtlı"
STR_MAC_ADDRESS: "MAC adresi:"
STR_CHECKING_WIFI: "WiFi kontrol ediliyor..."
+1 -1
View File
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Відкрийте цю URL-адресу у вашому б
STR_OR_HTTP_PREFIX: "або http://"
STR_SCAN_QR_HINT: "або відскануйте QR-код телефоном:"
STR_CALIBRE_WIRELESS: "Calibre бездротовий"
STR_CALIBRE_WEB_URL: "URL Calibre Web"
STR_CALIBRE_WEB_URL: "OPDS URL"
STR_NETWORK_LEGEND: "* = Зашифровано | + = Збережено"
STR_MAC_ADDRESS: "MAC адреса:"
STR_CHECKING_WIFI: "Перевірка WiFi..."
+53 -102
View File
@@ -25,15 +25,12 @@ OpdsParser::~OpdsParser() {
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;
}
if (errorOccured) return length;
XML_SetUserData(parser, this);
XML_SetElementHandler(parser, startElement, endElement);
XML_SetCharacterDataHandler(parser, characterData);
// Parse in chunks to avoid large buffer allocations
const char* currentPos = reinterpret_cast<const char*>(xmlData);
size_t remaining = length;
constexpr size_t chunkSize = 1024;
@@ -42,9 +39,7 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
void* const buf = XML_GetBuffer(parser, chunkSize);
if (!buf) {
errorOccured = true;
LOG_DBG("OPDS", "Couldn't allocate memory for buffer");
XML_ParserFree(parser);
parser = nullptr;
return length;
}
@@ -53,13 +48,9 @@ size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) {
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)));
XML_ParserFree(parser);
parser = nullptr;
return length;
}
currentPos += toRead;
remaining -= toRead;
}
@@ -78,30 +69,25 @@ bool OpdsParser::error() const { return errorOccured; }
void OpdsParser::clear() {
entries.clear();
searchTemplate.clear();
nextPageUrl.clear();
prevPageUrl.clear();
currentEntry = OpdsEntry{};
currentText.clear();
inEntry = false;
inTitle = false;
inAuthor = false;
inAuthorName = false;
inId = false;
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);
}
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];
}
if (strcmp(atts[i], name) == 0) return atts[i + 1];
}
return nullptr;
}
@@ -109,7 +95,38 @@ const char* OpdsParser::findAttribute(const XML_Char** atts, const char* name) {
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, "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) {
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{};
@@ -118,112 +135,46 @@ void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, cons
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) {
} else 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)) {
} else 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) {
} else 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) {
} 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;
}
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);
}
+6
View File
@@ -49,6 +49,9 @@ class OpdsParser final : public Print {
~OpdsParser();
// Disable copy
const std::string& getSearchTemplate() const { return searchTemplate; }
const std::string& getNextPageUrl() const { return nextPageUrl; }
const std::string& getPrevPageUrl() const { return prevPageUrl; }
OpdsParser(const OpdsParser&) = delete;
OpdsParser& operator=(const OpdsParser&) = delete;
@@ -85,6 +88,9 @@ class OpdsParser final : public Print {
static void XMLCALL endElement(void* userData, const XML_Char* name);
static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
std::string searchTemplate;
std::string nextPageUrl;
std::string prevPageUrl;
// Helper to find attribute value
static const char* findAttribute(const XML_Char** atts, const char* name);