Files
rxmmah fa2a3d2539 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>
2026-04-12 12:24:27 -04:00

111 lines
2.8 KiB
C++

#pragma once
#include <Print.h>
#include <expat.h>
#include <string>
#include <vector>
/**
* Type of OPDS entry.
*/
enum class OpdsEntryType {
NAVIGATION, // Link to another catalog
BOOK // Downloadable book
};
/**
* Represents an entry from an OPDS feed (either a navigation link or a book).
*/
struct OpdsEntry {
OpdsEntryType type = OpdsEntryType::NAVIGATION;
std::string title;
std::string author; // Only for books
std::string href; // Navigation URL or epub download URL
std::string id;
};
// Legacy alias for backward compatibility
using OpdsBook = OpdsEntry;
/**
* Parser for OPDS (Open Publication Distribution System) Atom feeds.
* Uses the Expat XML parser to parse OPDS catalog entries.
*
* Usage:
* OpdsParser parser;
* if (parser.parse(xmlData, xmlLength)) {
* for (const auto& entry : parser.getEntries()) {
* if (entry.type == OpdsEntryType::BOOK) {
* // Downloadable book
* } else {
* // Navigation link to another catalog
* }
* }
* }
*/
class OpdsParser final : public Print {
public:
OpdsParser();
~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;
size_t write(uint8_t) override;
size_t write(const uint8_t*, size_t) override;
void flush() override;
bool error() const;
operator bool() { return !error(); }
/**
* Get the parsed entries (both navigation and book entries).
* @return Vector of OpdsEntry entries
*/
const std::vector<OpdsEntry>& getEntries() const& { return entries; }
std::vector<OpdsEntry> getEntries() && { return std::move(entries); }
/**
* Get only book entries (legacy compatibility).
* @return Vector of book entries
*/
std::vector<OpdsEntry> getBooks() const;
/**
* Clear all parsed entries.
*/
void clear();
private:
// Expat callbacks
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
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);
XML_Parser parser = nullptr;
std::vector<OpdsEntry> entries;
OpdsEntry currentEntry;
std::string currentText;
// Parser state
bool inEntry = false;
bool inTitle = false;
bool inAuthor = false;
bool inAuthorName = false;
bool inId = false;
bool errorOccured = false;
};