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:
committed by
jpirnay
co-authored by
Claude Sonnet 4.6
parent
bd43221ad9
commit
b47e5b5fff
@@ -70,6 +70,7 @@ bool OpdsParser::error() const { return errorOccured; }
|
|||||||
void OpdsParser::clear() {
|
void OpdsParser::clear() {
|
||||||
entries.clear();
|
entries.clear();
|
||||||
searchTemplate.clear();
|
searchTemplate.clear();
|
||||||
|
osdUrl.clear();
|
||||||
nextPageUrl.clear();
|
nextPageUrl.clear();
|
||||||
prevPageUrl.clear();
|
prevPageUrl.clear();
|
||||||
currentEntry = OpdsEntry{};
|
currentEntry = OpdsEntry{};
|
||||||
@@ -105,6 +106,8 @@ void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, cons
|
|||||||
std::string sHref(href);
|
std::string sHref(href);
|
||||||
if (sHref.find("{searchTerms}") != std::string::npos) {
|
if (sHref.find("{searchTerms}") != std::string::npos) {
|
||||||
self->searchTemplate = sHref;
|
self->searchTemplate = sHref;
|
||||||
|
} else if (type && strcmp(type, "application/opensearchdescription+xml") == 0) {
|
||||||
|
self->osdUrl = sHref;
|
||||||
}
|
}
|
||||||
} else if (rel && strcmp(rel, "next") == 0 && !self->inEntry) {
|
} else if (rel && strcmp(rel, "next") == 0 && !self->inEntry) {
|
||||||
self->nextPageUrl = href;
|
self->nextPageUrl = href;
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class OpdsParser final : public Print {
|
|||||||
|
|
||||||
// Disable copy
|
// Disable copy
|
||||||
const std::string& getSearchTemplate() const { return searchTemplate; }
|
const std::string& getSearchTemplate() const { return searchTemplate; }
|
||||||
|
const std::string& getOsdUrl() const { return osdUrl; }
|
||||||
const std::string& getNextPageUrl() const { return nextPageUrl; }
|
const std::string& getNextPageUrl() const { return nextPageUrl; }
|
||||||
const std::string& getPrevPageUrl() const { return prevPageUrl; }
|
const std::string& getPrevPageUrl() const { return prevPageUrl; }
|
||||||
OpdsParser(const OpdsParser&) = delete;
|
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);
|
static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
|
||||||
|
|
||||||
std::string searchTemplate;
|
std::string searchTemplate;
|
||||||
|
std::string osdUrl;
|
||||||
std::string nextPageUrl;
|
std::string nextPageUrl;
|
||||||
std::string prevPageUrl;
|
std::string prevPageUrl;
|
||||||
// Helper to find attribute value
|
// Helper to find attribute value
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <OpdsStream.h>
|
#include <OpdsStream.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
#include <expat.h>
|
||||||
|
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@@ -245,6 +246,9 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
searchTemplate = parser.getSearchTemplate();
|
searchTemplate = parser.getSearchTemplate();
|
||||||
|
if (searchTemplate.empty() && !parser.getOsdUrl().empty()) {
|
||||||
|
fetchOsdTemplate(UrlUtils::buildUrl(url, parser.getOsdUrl()));
|
||||||
|
}
|
||||||
const auto& nextUrl = parser.getNextPageUrl();
|
const auto& nextUrl = parser.getNextPageUrl();
|
||||||
const auto& prevUrl = parser.getPrevPageUrl();
|
const auto& prevUrl = parser.getPrevPageUrl();
|
||||||
entries = std::move(parser).getEntries();
|
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() {
|
void OpdsBookBrowserActivity::launchSearch() {
|
||||||
consumeConfirm = true;
|
consumeConfirm = true;
|
||||||
state = BrowserState::SEARCH_INPUT;
|
state = BrowserState::SEARCH_INPUT;
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ class OpdsBookBrowserActivity final : public Activity {
|
|||||||
void navigateToEntry(const OpdsEntry& entry);
|
void navigateToEntry(const OpdsEntry& entry);
|
||||||
void navigateBack();
|
void navigateBack();
|
||||||
void downloadBook(const OpdsEntry& book);
|
void downloadBook(const OpdsEntry& book);
|
||||||
|
void fetchOsdTemplate(const std::string& osdUrl);
|
||||||
void launchSearch();
|
void launchSearch();
|
||||||
void performSearch(const std::string& query);
|
void performSearch(const std::string& query);
|
||||||
bool preventAutoSleep() override { return true; }
|
bool preventAutoSleep() override { return true; }
|
||||||
|
|||||||
Reference in New Issue
Block a user