feat: support two-step OpenSearch Description for OPDS search

Servers like copyparty advertise search via a link to an OpenSearch
Description (OSD) document rather than embedding {searchTerms} directly
in the feed's <link rel="search"> href (the Calibre-Web style).

OpdsParser now stores the OSD URL when rel="search" and
type="application/opensearchdescription+xml" but the href lacks
{searchTerms}. OpdsBookBrowserActivity fetches that OSD document after
each feed load and parses the <Url template="..."> element with a
minimal inline expat parser to extract the actual search template.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Brandon Philips
2026-04-15 17:58:02 +02:00
committed by jpirnay
co-authored by Claude Sonnet 4.6
parent bd43221ad9
commit b47e5b5fff
4 changed files with 46 additions and 0 deletions
+3
View File
@@ -70,6 +70,7 @@ bool OpdsParser::error() const { return errorOccured; }
void OpdsParser::clear() {
entries.clear();
searchTemplate.clear();
osdUrl.clear();
nextPageUrl.clear();
prevPageUrl.clear();
currentEntry = OpdsEntry{};
@@ -105,6 +106,8 @@ void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, cons
std::string sHref(href);
if (sHref.find("{searchTerms}") != std::string::npos) {
self->searchTemplate = sHref;
} else if (type && strcmp(type, "application/opensearchdescription+xml") == 0) {
self->osdUrl = sHref;
}
} else if (rel && strcmp(rel, "next") == 0 && !self->inEntry) {
self->nextPageUrl = href;
+2
View File
@@ -50,6 +50,7 @@ class OpdsParser final : public Print {
// Disable copy
const std::string& getSearchTemplate() const { return searchTemplate; }
const std::string& getOsdUrl() const { return osdUrl; }
const std::string& getNextPageUrl() const { return nextPageUrl; }
const std::string& getPrevPageUrl() const { return prevPageUrl; }
OpdsParser(const OpdsParser&) = delete;
@@ -89,6 +90,7 @@ class OpdsParser final : public Print {
static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
std::string searchTemplate;
std::string osdUrl;
std::string nextPageUrl;
std::string prevPageUrl;
// Helper to find attribute value
@@ -7,6 +7,7 @@
#include <Logging.h>
#include <OpdsStream.h>
#include <WiFi.h>
#include <expat.h>
#include <cctype>
#include <cstdio>
@@ -245,6 +246,9 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
}
searchTemplate = parser.getSearchTemplate();
if (searchTemplate.empty() && !parser.getOsdUrl().empty()) {
fetchOsdTemplate(UrlUtils::buildUrl(url, parser.getOsdUrl()));
}
const auto& nextUrl = parser.getNextPageUrl();
const auto& prevUrl = parser.getPrevPageUrl();
entries = std::move(parser).getEntries();
@@ -336,6 +340,42 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
}
}
void OpdsBookBrowserActivity::fetchOsdTemplate(const std::string& osdUrl) {
std::string content;
if (!HttpDownloader::fetchUrl(osdUrl, content)) {
LOG_ERR("OPDS", "Failed to fetch OSD: %s", osdUrl.c_str());
return;
}
struct OsdState {
std::string templateUrl;
static void XMLCALL onStart(void* ud, const XML_Char* name, const XML_Char** atts) {
if (strcmp(name, "Url") != 0 && strstr(name, ":Url") == nullptr) return;
auto* state = static_cast<OsdState*>(ud);
for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "template") == 0 && strstr(atts[i + 1], "{searchTerms}") != nullptr) {
state->templateUrl = atts[i + 1];
break;
}
}
}
} osdState;
XML_Parser p = XML_ParserCreate(nullptr);
if (!p) {
LOG_ERR("OPDS", "OSD parser alloc failed");
return;
}
XML_SetUserData(p, &osdState);
XML_SetElementHandler(p, OsdState::onStart, nullptr);
XML_Parse(p, content.c_str(), static_cast<int>(content.size()), XML_TRUE);
XML_ParserFree(p);
if (!osdState.templateUrl.empty()) {
searchTemplate = UrlUtils::buildUrl(osdUrl, osdState.templateUrl);
}
}
void OpdsBookBrowserActivity::launchSearch() {
consumeConfirm = true;
state = BrowserState::SEARCH_INPUT;
@@ -46,6 +46,7 @@ class OpdsBookBrowserActivity final : public Activity {
void navigateToEntry(const OpdsEntry& entry);
void navigateBack();
void downloadBook(const OpdsEntry& book);
void fetchOsdTemplate(const std::string& osdUrl);
void launchSearch();
void performSearch(const std::string& query);
bool preventAutoSleep() override { return true; }