Merge pull request #194 from jpirnay/fix-opds-oom
feat: Support OPDS image sidecar download and fix OOM for large lists
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Returns the length of href after trimming trailing slashes.
|
// Returns the length of href after trimming trailing slashes.
|
||||||
@@ -146,7 +147,6 @@ void OpdsParser::flush() {
|
|||||||
bool OpdsParser::error() const { return errorOccured; }
|
bool OpdsParser::error() const { return errorOccured; }
|
||||||
|
|
||||||
void OpdsParser::clear() {
|
void OpdsParser::clear() {
|
||||||
entries.clear();
|
|
||||||
searchTemplate.clear();
|
searchTemplate.clear();
|
||||||
osdUrl.clear();
|
osdUrl.clear();
|
||||||
nextPageUrl.clear();
|
nextPageUrl.clear();
|
||||||
@@ -156,14 +156,6 @@ void OpdsParser::clear() {
|
|||||||
inEntry = inTitle = inAuthor = inAuthorName = 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);
|
|
||||||
}
|
|
||||||
return books;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* OpdsParser::findAttribute(const XML_Char** atts, const char* name) {
|
const char* OpdsParser::findAttribute(const XML_Char** atts, const char* name) {
|
||||||
for (int i = 0; atts[i]; i += 2) {
|
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];
|
||||||
@@ -206,6 +198,10 @@ void XMLCALL OpdsParser::startElement(void* userData, const XML_Char* name, cons
|
|||||||
}
|
}
|
||||||
self->currentEntry.acquisitionLinks.push_back(acquisition);
|
self->currentEntry.acquisitionLinks.push_back(acquisition);
|
||||||
}
|
}
|
||||||
|
} else if (rel && type && strstr(rel, "opds-spec.org/image") != nullptr &&
|
||||||
|
strstr(rel, "thumbnail") == nullptr && strncmp(type, "image/", 6) == 0 &&
|
||||||
|
self->currentEntry.imageHref.empty()) {
|
||||||
|
self->currentEntry.imageHref = href;
|
||||||
} else if (type && strstr(type, "application/atom+xml") != nullptr) {
|
} else if (type && strstr(type, "application/atom+xml") != nullptr) {
|
||||||
if (self->currentEntry.type != OpdsEntryType::BOOK) {
|
if (self->currentEntry.type != OpdsEntryType::BOOK) {
|
||||||
self->currentEntry.type = OpdsEntryType::NAVIGATION;
|
self->currentEntry.type = OpdsEntryType::NAVIGATION;
|
||||||
@@ -243,7 +239,10 @@ void XMLCALL OpdsParser::endElement(void* userData, const XML_Char* name) {
|
|||||||
|
|
||||||
if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) {
|
if (strcmp(name, "entry") == 0 || strstr(name, ":entry") != nullptr) {
|
||||||
if (!self->currentEntry.title.empty() && !self->currentEntry.href.empty()) {
|
if (!self->currentEntry.title.empty() && !self->currentEntry.href.empty()) {
|
||||||
self->entries.push_back(self->currentEntry);
|
if (self->onEntryParsed) {
|
||||||
|
self->onEntryParsed(std::move(self->currentEntry));
|
||||||
|
self->currentEntry = OpdsEntry{};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self->inEntry = false;
|
self->inEntry = false;
|
||||||
} else if (self->inEntry) {
|
} else if (self->inEntry) {
|
||||||
|
|||||||
+14
-21
@@ -2,6 +2,7 @@
|
|||||||
#include <Print.h>
|
#include <Print.h>
|
||||||
#include <expat.h>
|
#include <expat.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ struct OpdsEntry {
|
|||||||
std::string href; // Navigation URL or epub download URL
|
std::string href; // Navigation URL or epub download URL
|
||||||
std::string id;
|
std::string id;
|
||||||
std::vector<OpdsAcquisitionLink> acquisitionLinks;
|
std::vector<OpdsAcquisitionLink> acquisitionLinks;
|
||||||
|
std::string imageHref; // Cover image URL (rel="http://opds-spec.org/image"), books only
|
||||||
};
|
};
|
||||||
|
|
||||||
// Legacy alias for backward compatibility
|
// Legacy alias for backward compatibility
|
||||||
@@ -41,14 +43,17 @@ using OpdsBook = OpdsEntry;
|
|||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* OpdsParser parser;
|
* OpdsParser parser;
|
||||||
* if (parser.parse(xmlData, xmlLength)) {
|
* parser.onEntryParsed = [](OpdsEntry entry) {
|
||||||
* for (const auto& entry : parser.getEntries()) {
|
* if (entry.type == OpdsEntryType::BOOK) {
|
||||||
* if (entry.type == OpdsEntryType::BOOK) {
|
* // Process downloadable book
|
||||||
* // Downloadable book
|
* } else {
|
||||||
* } else {
|
* // Process navigation link
|
||||||
* // Navigation link to another catalog
|
|
||||||
* }
|
|
||||||
* }
|
* }
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* // Entries are emitted immediately as they are parsed from the stream.
|
||||||
|
* if (parser.parse(xmlData, xmlLength)) {
|
||||||
|
* // Parsing completed successfully
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
class OpdsParser final : public Print {
|
class OpdsParser final : public Print {
|
||||||
@@ -73,24 +78,13 @@ class OpdsParser final : public Print {
|
|||||||
|
|
||||||
operator bool() { return !error(); }
|
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.
|
* Clear all parsed entries.
|
||||||
*/
|
*/
|
||||||
void clear();
|
void clear();
|
||||||
|
|
||||||
|
std::function<void(OpdsEntry)> onEntryParsed;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Expat callbacks
|
// Expat callbacks
|
||||||
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||||
@@ -105,7 +99,6 @@ class OpdsParser final : public Print {
|
|||||||
static const char* findAttribute(const XML_Char** atts, const char* name);
|
static const char* findAttribute(const XML_Char** atts, const char* name);
|
||||||
|
|
||||||
XML_Parser parser = nullptr;
|
XML_Parser parser = nullptr;
|
||||||
std::vector<OpdsEntry> entries;
|
|
||||||
OpdsEntry currentEntry;
|
OpdsEntry currentEntry;
|
||||||
std::string currentText;
|
std::string currentText;
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ int ButtonEventManager::buttonToIndex(const Button button) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ButtonEventManager::hasDoubleAction(const Button button) {
|
bool ButtonEventManager::hasDoubleAction(const Button button) const {
|
||||||
|
if (forcedDoubleMask & (1 << static_cast<int>(button))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
using BA = CrossPointSettings::BUTTON_ACTION;
|
using BA = CrossPointSettings::BUTTON_ACTION;
|
||||||
switch (button) {
|
switch (button) {
|
||||||
case Button::Back:
|
case Button::Back:
|
||||||
|
|||||||
@@ -49,6 +49,16 @@ class ButtonEventManager {
|
|||||||
// Reset all per-button FSMs. Call on activity transitions to prevent bleed-through.
|
// Reset all per-button FSMs. Call on activity transitions to prevent bleed-through.
|
||||||
void drain();
|
void drain();
|
||||||
|
|
||||||
|
// Temporarily force double-click detection for a button (adds latency to Short press).
|
||||||
|
// Call this in the Activity's transition setup or loop.
|
||||||
|
void forceDoubleAction(Button button, bool enable = true) {
|
||||||
|
if (enable) {
|
||||||
|
forcedDoubleMask |= (1 << static_cast<int>(button));
|
||||||
|
} else {
|
||||||
|
forcedDoubleMask &= ~(1 << static_cast<int>(button));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Preserve a default event for activity processing after main loop dispatch.
|
// Preserve a default event for activity processing after main loop dispatch.
|
||||||
// This is used when the configured action is BTN_DEFAULT.
|
// This is used when the configured action is BTN_DEFAULT.
|
||||||
void pushEventFront(Button button, PressType type);
|
void pushEventFront(Button button, PressType type);
|
||||||
@@ -59,14 +69,17 @@ class ButtonEventManager {
|
|||||||
|
|
||||||
// Returns true if a double-click action is configured for this button.
|
// Returns true if a double-click action is configured for this button.
|
||||||
// ButtonEventManager queries CrossPointSettings internally.
|
// ButtonEventManager queries CrossPointSettings internally.
|
||||||
static bool hasDoubleAction(Button button);
|
bool hasDoubleAction(Button button) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr int NUM_BUTTONS = 7;
|
static constexpr int NUM_BUTTONS = 9;
|
||||||
static constexpr Button ALL_BUTTONS[NUM_BUTTONS] = {
|
static constexpr Button ALL_BUTTONS[NUM_BUTTONS] = {
|
||||||
Button::Back, Button::Confirm, Button::Left, Button::Right, Button::PageBack, Button::PageForward, Button::Power,
|
Button::Back, Button::Confirm, Button::Left, Button::Right, Button::Up,
|
||||||
|
Button::Down, Button::PageBack, Button::PageForward, Button::Power,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
uint32_t forcedDoubleMask = 0;
|
||||||
|
|
||||||
enum class State { Idle, Pressed, ReleasedOnce, DoublePressed };
|
enum class State { Idle, Pressed, ReleasedOnce, DoublePressed };
|
||||||
|
|
||||||
struct PerButton {
|
struct PerButton {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <Epub.h>
|
#include <Epub.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HalClock.h>
|
#include <HalClock.h>
|
||||||
|
#include <HalStorage.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <OpdsStream.h>
|
#include <OpdsStream.h>
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
#include "ButtonEventManager.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "OpdsFormatLabel.h"
|
#include "OpdsFormatLabel.h"
|
||||||
#include "activities/network/WifiSelectionActivity.h"
|
#include "activities/network/WifiSelectionActivity.h"
|
||||||
@@ -39,13 +41,83 @@ int formatItemsPerPage(const Rect& contentRect) {
|
|||||||
const int itemsPerPage = availableHeight / FORMAT_ITEM_HEIGHT;
|
const int itemsPerPage = availableHeight / FORMAT_ITEM_HEIGHT;
|
||||||
return itemsPerPage > 0 ? itemsPerPage : 1;
|
return itemsPerPage > 0 ? itemsPerPage : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void writeString(HalFile& f, const std::string& s) {
|
||||||
|
uint16_t len = s.length();
|
||||||
|
f.write(reinterpret_cast<const uint8_t*>(&len), sizeof(len));
|
||||||
|
if (len > 0) f.write(reinterpret_cast<const void*>(s.data()), len);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string readString(HalFile& f) {
|
||||||
|
uint16_t len = 0;
|
||||||
|
if (f.read(&len, sizeof(len)) != sizeof(len)) return "";
|
||||||
|
if (len == 0) return "";
|
||||||
|
std::string s(len, '\0');
|
||||||
|
f.read(s.data(), len);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
void writeEntryToCache(HalFile& f, const OpdsEntry& entry) {
|
||||||
|
uint8_t type = static_cast<uint8_t>(entry.type);
|
||||||
|
f.write(&type, sizeof(type));
|
||||||
|
writeString(f, entry.title);
|
||||||
|
writeString(f, entry.author);
|
||||||
|
writeString(f, entry.href);
|
||||||
|
writeString(f, entry.id);
|
||||||
|
writeString(f, entry.imageHref);
|
||||||
|
uint16_t numLinks = entry.acquisitionLinks.size();
|
||||||
|
f.write(reinterpret_cast<const uint8_t*>(&numLinks), sizeof(numLinks));
|
||||||
|
for (const auto& link : entry.acquisitionLinks) {
|
||||||
|
writeString(f, link.href);
|
||||||
|
writeString(f, link.mimeType);
|
||||||
|
writeString(f, link.formatKey);
|
||||||
|
writeString(f, link.fileExtension);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OpdsEntry readEntryFromCache(HalFile& f) {
|
||||||
|
OpdsEntry entry;
|
||||||
|
uint8_t type = 0;
|
||||||
|
if (f.read(&type, sizeof(type)) == sizeof(type)) {
|
||||||
|
entry.type = static_cast<OpdsEntryType>(type);
|
||||||
|
}
|
||||||
|
entry.title = readString(f);
|
||||||
|
entry.author = readString(f);
|
||||||
|
entry.href = readString(f);
|
||||||
|
entry.id = readString(f);
|
||||||
|
entry.imageHref = readString(f);
|
||||||
|
uint16_t numLinks = 0;
|
||||||
|
if (f.read(&numLinks, sizeof(numLinks)) == sizeof(numLinks)) {
|
||||||
|
for (uint16_t i = 0; i < numLinks; ++i) {
|
||||||
|
OpdsAcquisitionLink link;
|
||||||
|
link.href = readString(f);
|
||||||
|
link.mimeType = readString(f);
|
||||||
|
link.formatKey = readString(f);
|
||||||
|
link.fileExtension = readString(f);
|
||||||
|
entry.acquisitionLinks.push_back(link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
OpdsEntry OpdsBookBrowserActivity::getEntry(size_t index) const {
|
||||||
|
if (index >= entryOffsets.size()) return {};
|
||||||
|
HalFile f;
|
||||||
|
if (!Storage.openFileForRead("OPDS", "/.tmp_opds_cache.dat", f)) return {};
|
||||||
|
f.seek(entryOffsets[index]);
|
||||||
|
OpdsEntry entry = readEntryFromCache(f);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
void OpdsBookBrowserActivity::onEnter() {
|
void OpdsBookBrowserActivity::onEnter() {
|
||||||
Activity::onEnter();
|
Activity::onEnter();
|
||||||
|
|
||||||
|
globalButtonEvents().forceDoubleAction(ButtonEventManager::Button::Up, true);
|
||||||
|
globalButtonEvents().forceDoubleAction(ButtonEventManager::Button::Down, true);
|
||||||
|
|
||||||
state = BrowserState::CHECK_WIFI;
|
state = BrowserState::CHECK_WIFI;
|
||||||
entries.clear();
|
entryOffsets.clear();
|
||||||
navigationHistory.clear();
|
navigationHistory.clear();
|
||||||
currentPath = ""; // Root path - user provides full URL in settings
|
currentPath = ""; // Root path - user provides full URL in settings
|
||||||
searchTemplate.clear();
|
searchTemplate.clear();
|
||||||
@@ -65,11 +137,15 @@ void OpdsBookBrowserActivity::onEnter() {
|
|||||||
void OpdsBookBrowserActivity::onExit() {
|
void OpdsBookBrowserActivity::onExit() {
|
||||||
Activity::onExit();
|
Activity::onExit();
|
||||||
|
|
||||||
|
globalButtonEvents().forceDoubleAction(ButtonEventManager::Button::Up, false);
|
||||||
|
globalButtonEvents().forceDoubleAction(ButtonEventManager::Button::Down, false);
|
||||||
|
|
||||||
HalClock::wifiOff();
|
HalClock::wifiOff();
|
||||||
|
|
||||||
entries.clear();
|
entryOffsets.clear();
|
||||||
navigationHistory.clear();
|
navigationHistory.clear();
|
||||||
formatSelectionLabels.clear();
|
formatSelectionLabels.clear();
|
||||||
|
Storage.remove("/.tmp_opds_cache.dat");
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpdsBookBrowserActivity::loop() {
|
void OpdsBookBrowserActivity::loop() {
|
||||||
@@ -112,13 +188,13 @@ void OpdsBookBrowserActivity::loop() {
|
|||||||
if (state == BrowserState::DOWNLOADING) return;
|
if (state == BrowserState::DOWNLOADING) return;
|
||||||
|
|
||||||
if (state == BrowserState::FORMAT_SELECTION) {
|
if (state == BrowserState::FORMAT_SELECTION) {
|
||||||
if (selectedBookIndex < 0 || selectedBookIndex >= static_cast<int>(entries.size())) {
|
if (selectedBookIndex < 0 || selectedBookIndex >= static_cast<int>(entryOffsets.size())) {
|
||||||
state = BrowserState::BROWSING;
|
state = BrowserState::BROWSING;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& entry = entries[selectedBookIndex];
|
const auto entry = getEntry(selectedBookIndex);
|
||||||
if (entry.acquisitionLinks.empty()) {
|
if (entry.acquisitionLinks.empty()) {
|
||||||
state = BrowserState::BROWSING;
|
state = BrowserState::BROWSING;
|
||||||
selectedBookIndex = -1;
|
selectedBookIndex = -1;
|
||||||
@@ -140,11 +216,11 @@ void OpdsBookBrowserActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
buttonNavigator.onNextRelease([this, &entry] {
|
buttonNavigator.onNextRelease([this, entry] {
|
||||||
formatSelectorIndex = ButtonNavigator::nextIndex(formatSelectorIndex, entry.acquisitionLinks.size());
|
formatSelectorIndex = ButtonNavigator::nextIndex(formatSelectorIndex, entry.acquisitionLinks.size());
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
});
|
});
|
||||||
buttonNavigator.onPreviousRelease([this, &entry] {
|
buttonNavigator.onPreviousRelease([this, entry] {
|
||||||
formatSelectorIndex = ButtonNavigator::previousIndex(formatSelectorIndex, entry.acquisitionLinks.size());
|
formatSelectorIndex = ButtonNavigator::previousIndex(formatSelectorIndex, entry.acquisitionLinks.size());
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
});
|
});
|
||||||
@@ -153,8 +229,8 @@ void OpdsBookBrowserActivity::loop() {
|
|||||||
|
|
||||||
if (state == BrowserState::BROWSING) {
|
if (state == BrowserState::BROWSING) {
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
if (!entries.empty()) {
|
if (!entryOffsets.empty()) {
|
||||||
const auto& entry = entries[selectorIndex];
|
const auto entry = getEntry(selectorIndex);
|
||||||
entry.type == OpdsEntryType::BOOK ? chooseBookFormat(entry) : navigateToEntry(entry);
|
entry.type == OpdsEntryType::BOOK ? chooseBookFormat(entry) : navigateToEntry(entry);
|
||||||
}
|
}
|
||||||
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
@@ -163,24 +239,36 @@ void OpdsBookBrowserActivity::loop() {
|
|||||||
if (!searchTemplate.empty() && selectorIndex == 0) launchSearch();
|
if (!searchTemplate.empty() && selectorIndex == 0) launchSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!entries.empty()) {
|
if (!entryOffsets.empty()) {
|
||||||
// Navigator is restricted to Up/Down so a Left release used to launch
|
ButtonEventManager::ButtonEvent extEvent;
|
||||||
// search (line above) cannot also be consumed here as a previous-item
|
while (globalButtonEvents().consumeEvent(extEvent)) {
|
||||||
// step on the same tick.
|
if (extEvent.type == ButtonEventManager::PressType::Double) {
|
||||||
buttonNavigator.onRelease({MappedInputManager::Button::Down}, [this] {
|
if (extEvent.button == ButtonEventManager::Button::Down) {
|
||||||
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entries.size());
|
selectorIndex = (selectorIndex + 9) % entryOffsets.size();
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
});
|
} else if (extEvent.button == ButtonEventManager::Button::Up) {
|
||||||
buttonNavigator.onRelease({MappedInputManager::Button::Up}, [this] {
|
int size = entryOffsets.size();
|
||||||
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entries.size());
|
selectorIndex = (selectorIndex - 9 + size) % size;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
});
|
}
|
||||||
|
} else if (extEvent.type == ButtonEventManager::PressType::Short ||
|
||||||
|
extEvent.type == ButtonEventManager::PressType::Long) {
|
||||||
|
if (extEvent.button == ButtonEventManager::Button::Down) {
|
||||||
|
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entryOffsets.size());
|
||||||
|
requestUpdate();
|
||||||
|
} else if (extEvent.button == ButtonEventManager::Button::Up) {
|
||||||
|
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entryOffsets.size());
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buttonNavigator.onContinuous({MappedInputManager::Button::Down}, [this] {
|
buttonNavigator.onContinuous({MappedInputManager::Button::Down}, [this] {
|
||||||
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entries.size(), PAGE_ITEMS);
|
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entryOffsets.size(), PAGE_ITEMS);
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
});
|
});
|
||||||
buttonNavigator.onContinuous({MappedInputManager::Button::Up}, [this] {
|
buttonNavigator.onContinuous({MappedInputManager::Button::Up}, [this] {
|
||||||
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, entries.size(), PAGE_ITEMS);
|
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, entryOffsets.size(), PAGE_ITEMS);
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -239,7 +327,7 @@ void OpdsBookBrowserActivity::render(RenderLock&&) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state == BrowserState::FORMAT_SELECTION) {
|
if (state == BrowserState::FORMAT_SELECTION) {
|
||||||
const auto& entry = entries[selectedBookIndex];
|
const auto entry = getEntry(selectedBookIndex);
|
||||||
auto title = renderer.truncatedText(UI_10_FONT_ID, entry.title.c_str(), contentRect.width - 40);
|
auto title = renderer.truncatedText(UI_10_FONT_ID, entry.title.c_str(), contentRect.width - 40);
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, midY - 40, title.c_str(), true, EpdFontFamily::BOLD);
|
renderer.drawCenteredText(UI_10_FONT_ID, midY - 40, title.c_str(), true, EpdFontFamily::BOLD);
|
||||||
if (!entry.author.empty()) {
|
if (!entry.author.empty()) {
|
||||||
@@ -269,12 +357,12 @@ void OpdsBookBrowserActivity::render(RenderLock&&) {
|
|||||||
// Browsing state
|
// Browsing state
|
||||||
// Show appropriate button hint based on selected entry type
|
// Show appropriate button hint based on selected entry type
|
||||||
const char* confirmLabel =
|
const char* confirmLabel =
|
||||||
(!entries.empty() && entries[selectorIndex].type == OpdsEntryType::BOOK) ? tr(STR_DOWNLOAD) : tr(STR_OPEN);
|
(!entryOffsets.empty() && getEntry(selectorIndex).type == OpdsEntryType::BOOK) ? tr(STR_DOWNLOAD) : tr(STR_OPEN);
|
||||||
const char* searchLabel = (!searchTemplate.empty() && selectorIndex == 0) ? tr(STR_SEARCH) : tr(STR_DIR_UP);
|
const char* searchLabel = (!searchTemplate.empty() && selectorIndex == 0) ? tr(STR_SEARCH) : tr(STR_DIR_UP);
|
||||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel, searchLabel, tr(STR_DIR_DOWN));
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel, searchLabel, tr(STR_DIR_DOWN));
|
||||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
if (entries.empty()) {
|
if (entryOffsets.empty()) {
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, midY, tr(STR_NO_ENTRIES));
|
renderer.drawCenteredText(UI_10_FONT_ID, midY, tr(STR_NO_ENTRIES));
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
return;
|
return;
|
||||||
@@ -283,8 +371,9 @@ void OpdsBookBrowserActivity::render(RenderLock&&) {
|
|||||||
const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS;
|
const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS;
|
||||||
renderer.fillRect(contentRect.x, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, contentRect.width - 1, 30);
|
renderer.fillRect(contentRect.x, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, contentRect.width - 1, 30);
|
||||||
|
|
||||||
for (size_t i = pageStartIndex; i < entries.size() && i < static_cast<size_t>(pageStartIndex + PAGE_ITEMS); i++) {
|
for (size_t i = pageStartIndex; i < entryOffsets.size() && i < static_cast<size_t>(pageStartIndex + PAGE_ITEMS);
|
||||||
const auto& entry = entries[i];
|
i++) {
|
||||||
|
const auto entry = getEntry(i);
|
||||||
|
|
||||||
// Format display text with type indicator
|
// Format display text with type indicator
|
||||||
std::string displayText;
|
std::string displayText;
|
||||||
@@ -314,10 +403,27 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entryOffsets.clear();
|
||||||
|
|
||||||
std::string url = (path.find("http") == 0) ? path : UrlUtils::buildUrl(server.url, path);
|
std::string url = (path.find("http") == 0) ? path : UrlUtils::buildUrl(server.url, path);
|
||||||
LOG_DBG("OPDS", "Fetching: %s", url.c_str());
|
LOG_DBG("OPDS", "Fetching: %s", url.c_str());
|
||||||
|
|
||||||
OpdsParser parser;
|
OpdsParser parser;
|
||||||
|
HalFile cacheFile;
|
||||||
|
|
||||||
|
if (!Storage.openFileForWrite("OPDS", "/.tmp_opds_cache.dat", cacheFile)) {
|
||||||
|
LOG_ERR("OPDS", "Could not open cache file");
|
||||||
|
state = BrowserState::ERROR;
|
||||||
|
errorMessage = "Cache Error";
|
||||||
|
requestUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.onEntryParsed = [&](const OpdsEntry& entry) {
|
||||||
|
uint32_t offset = cacheFile.position();
|
||||||
|
entryOffsets.push_back(offset);
|
||||||
|
writeEntryToCache(cacheFile, entry);
|
||||||
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
OpdsParserStream stream{parser};
|
OpdsParserStream stream{parser};
|
||||||
@@ -342,18 +448,23 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
|
|||||||
}
|
}
|
||||||
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();
|
|
||||||
|
|
||||||
if (!prevUrl.empty()) {
|
if (!prevUrl.empty()) {
|
||||||
entries.insert(entries.begin(), OpdsEntry{OpdsEntryType::NAVIGATION, tr(STR_PREV_PAGE), "", prevUrl, ""});
|
std::string resolvedPrevUrl = UrlUtils::buildUrl(url, prevUrl);
|
||||||
|
OpdsEntry prevEntry{OpdsEntryType::NAVIGATION, tr(STR_PREV_PAGE), "", resolvedPrevUrl, ""};
|
||||||
|
entryOffsets.insert(entryOffsets.begin(), cacheFile.position());
|
||||||
|
writeEntryToCache(cacheFile, prevEntry);
|
||||||
}
|
}
|
||||||
if (!nextUrl.empty()) {
|
if (!nextUrl.empty()) {
|
||||||
entries.push_back(OpdsEntry{OpdsEntryType::NAVIGATION, tr(STR_NEXT_PAGE), "", nextUrl, ""});
|
std::string resolvedNextUrl = UrlUtils::buildUrl(url, nextUrl);
|
||||||
|
OpdsEntry nextEntry{OpdsEntryType::NAVIGATION, tr(STR_NEXT_PAGE), "", resolvedNextUrl, ""};
|
||||||
|
entryOffsets.push_back(cacheFile.position());
|
||||||
|
writeEntryToCache(cacheFile, nextEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
state = entries.empty() ? BrowserState::ERROR : BrowserState::BROWSING;
|
state = entryOffsets.empty() ? BrowserState::ERROR : BrowserState::BROWSING;
|
||||||
if (entries.empty()) errorMessage = tr(STR_NO_ENTRIES);
|
if (entryOffsets.empty()) errorMessage = tr(STR_NO_ENTRIES);
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +473,7 @@ void OpdsBookBrowserActivity::navigateToEntry(const OpdsEntry& entry) {
|
|||||||
currentPath = entry.href;
|
currentPath = entry.href;
|
||||||
state = BrowserState::LOADING;
|
state = BrowserState::LOADING;
|
||||||
statusMessage = tr(STR_LOADING);
|
statusMessage = tr(STR_LOADING);
|
||||||
entries.clear();
|
entryOffsets.clear();
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
requestUpdate(true);
|
requestUpdate(true);
|
||||||
fetchFeed(currentPath);
|
fetchFeed(currentPath);
|
||||||
@@ -376,7 +487,7 @@ void OpdsBookBrowserActivity::navigateBack() {
|
|||||||
navigationHistory.pop_back();
|
navigationHistory.pop_back();
|
||||||
state = BrowserState::LOADING;
|
state = BrowserState::LOADING;
|
||||||
statusMessage = tr(STR_LOADING);
|
statusMessage = tr(STR_LOADING);
|
||||||
entries.clear();
|
entryOffsets.clear();
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
fetchFeed(currentPath);
|
fetchFeed(currentPath);
|
||||||
@@ -451,7 +562,37 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book, const OpdsAcqu
|
|||||||
// Clear any existing cache for this book just in case it's a redownload of
|
// Clear any existing cache for this book just in case it's a redownload of
|
||||||
// a previously opened book.
|
// a previously opened book.
|
||||||
if (acquisition.mimeType == "application/epub+zip") {
|
if (acquisition.mimeType == "application/epub+zip") {
|
||||||
Epub(filename, "/.crosspoint").clearCache();
|
if (!book.imageHref.empty()) {
|
||||||
|
const std::string coverUrl =
|
||||||
|
(book.imageHref.rfind("http", 0) == 0) ? book.imageHref : UrlUtils::buildUrl(server.url, book.imageHref);
|
||||||
|
|
||||||
|
std::string baseFilename = filename;
|
||||||
|
size_t dotPos = baseFilename.find_last_of('.');
|
||||||
|
if (dotPos != std::string::npos) {
|
||||||
|
baseFilename.resize(dotPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ext = ".jpg";
|
||||||
|
if (book.imageHref.length() >= 4) {
|
||||||
|
std::string lowerHref = book.imageHref.substr(book.imageHref.length() - 4);
|
||||||
|
std::transform(lowerHref.begin(), lowerHref.end(), lowerHref.begin(), ::tolower);
|
||||||
|
if (lowerHref == ".png") {
|
||||||
|
ext = ".png";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::string sidecarPath = baseFilename + ext;
|
||||||
|
|
||||||
|
const auto coverDlResult = HttpDownloader::downloadToFile(
|
||||||
|
coverUrl, sidecarPath, [this](const size_t, const size_t) { requestUpdate(true); }, server.username,
|
||||||
|
server.password);
|
||||||
|
if (coverDlResult != HttpDownloader::OK) {
|
||||||
|
LOG_ERR("OPDS", "Failed to download cover from %s (err %d)", coverUrl.c_str(), (int)coverDlResult);
|
||||||
|
Storage.remove(sidecarPath.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Epub epub(filename, "/.crosspoint");
|
||||||
|
epub.clearCache();
|
||||||
} else if (acquisition.formatKey == "xtc" || acquisition.formatKey == "xtch") {
|
} else if (acquisition.formatKey == "xtc" || acquisition.formatKey == "xtch") {
|
||||||
Xtc(filename, "/.crosspoint").clearCache();
|
Xtc(filename, "/.crosspoint").clearCache();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class OpdsBookBrowserActivity final : public Activity {
|
|||||||
private:
|
private:
|
||||||
ButtonNavigator buttonNavigator;
|
ButtonNavigator buttonNavigator;
|
||||||
BrowserState state = BrowserState::LOADING;
|
BrowserState state = BrowserState::LOADING;
|
||||||
std::vector<OpdsEntry> entries;
|
std::vector<uint32_t> entryOffsets;
|
||||||
std::vector<std::string> navigationHistory;
|
std::vector<std::string> navigationHistory;
|
||||||
std::string currentPath;
|
std::string currentPath;
|
||||||
std::string searchTemplate;
|
std::string searchTemplate;
|
||||||
@@ -54,6 +54,8 @@ class OpdsBookBrowserActivity final : public Activity {
|
|||||||
|
|
||||||
OpdsServer server; // Copied at construction — safe even if the store changes during browsing
|
OpdsServer server; // Copied at construction — safe even if the store changes during browsing
|
||||||
|
|
||||||
|
OpdsEntry getEntry(size_t index) const;
|
||||||
|
|
||||||
void checkAndConnectWifi();
|
void checkAndConnectWifi();
|
||||||
void launchWifiSelection();
|
void launchWifiSelection();
|
||||||
void onWifiSelectionComplete(bool connected);
|
void onWifiSelectionComplete(bool connected);
|
||||||
|
|||||||
@@ -328,16 +328,16 @@ void EpubReaderActivity::loop() {
|
|||||||
|
|
||||||
if (ev.type == ButtonEventManager::PressType::Short) {
|
if (ev.type == ButtonEventManager::PressType::Short) {
|
||||||
if ((ev.button == MappedInputManager::Button::PageBack && SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
|
if ((ev.button == MappedInputManager::Button::PageBack && SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
|
||||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack)) ||
|
globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageBack)) ||
|
||||||
(ev.button == MappedInputManager::Button::Left && SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
|
(ev.button == MappedInputManager::Button::Left && SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
|
||||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left))) {
|
globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Left))) {
|
||||||
delayedPrevTurn = true;
|
delayedPrevTurn = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ((ev.button == MappedInputManager::Button::PageForward && SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
|
if ((ev.button == MappedInputManager::Button::PageForward && SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
|
||||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward)) ||
|
globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageForward)) ||
|
||||||
(ev.button == MappedInputManager::Button::Right && SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
|
(ev.button == MappedInputManager::Button::Right && SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
|
||||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right))) {
|
globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Right))) {
|
||||||
delayedNextTurn = true;
|
delayedNextTurn = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,16 +95,16 @@ inline PageTurnResult detectPageTurn(const MappedInputManager& input) {
|
|||||||
// because the button event system delays short events until the double-click window expires.
|
// because the button event system delays short events until the double-click window expires.
|
||||||
using BA = CrossPointSettings::BUTTON_ACTION;
|
using BA = CrossPointSettings::BUTTON_ACTION;
|
||||||
const bool prev = (SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
|
const bool prev = (SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
|
||||||
!ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack) &&
|
!globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageBack) &&
|
||||||
input.wasReleased(MappedInputManager::Button::PageBack)) ||
|
input.wasReleased(MappedInputManager::Button::PageBack)) ||
|
||||||
(SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
|
(SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
|
||||||
!ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left) &&
|
!globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Left) &&
|
||||||
input.wasReleased(MappedInputManager::Button::Left));
|
input.wasReleased(MappedInputManager::Button::Left));
|
||||||
const bool next = (SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
|
const bool next = (SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
|
||||||
!ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward) &&
|
!globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageForward) &&
|
||||||
input.wasReleased(MappedInputManager::Button::PageForward)) ||
|
input.wasReleased(MappedInputManager::Button::PageForward)) ||
|
||||||
(SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
|
(SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
|
||||||
!ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right) &&
|
!globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Right) &&
|
||||||
input.wasReleased(MappedInputManager::Button::Right));
|
input.wasReleased(MappedInputManager::Button::Right));
|
||||||
return {prev, next};
|
return {prev, next};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,16 +102,16 @@ void XtcReaderActivity::loop() {
|
|||||||
|
|
||||||
if (ev.type == ButtonEventManager::PressType::Short) {
|
if (ev.type == ButtonEventManager::PressType::Short) {
|
||||||
if ((ev.button == MappedInputManager::Button::PageBack && SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
|
if ((ev.button == MappedInputManager::Button::PageBack && SETTINGS.btnShortPageBack == BA::BTN_DEFAULT &&
|
||||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageBack)) ||
|
globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageBack)) ||
|
||||||
(ev.button == MappedInputManager::Button::Left && SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
|
(ev.button == MappedInputManager::Button::Left && SETTINGS.btnShortLeft == BA::BTN_DEFAULT &&
|
||||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Left))) {
|
globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Left))) {
|
||||||
delayedPrevTurn = true;
|
delayedPrevTurn = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ((ev.button == MappedInputManager::Button::PageForward && SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
|
if ((ev.button == MappedInputManager::Button::PageForward && SETTINGS.btnShortPageForward == BA::BTN_DEFAULT &&
|
||||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::PageForward)) ||
|
globalButtonEvents().hasDoubleAction(MappedInputManager::Button::PageForward)) ||
|
||||||
(ev.button == MappedInputManager::Button::Right && SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
|
(ev.button == MappedInputManager::Button::Right && SETTINGS.btnShortRight == BA::BTN_DEFAULT &&
|
||||||
ButtonEventManager::hasDoubleAction(MappedInputManager::Button::Right))) {
|
globalButtonEvents().hasDoubleAction(MappedInputManager::Button::Right))) {
|
||||||
delayedNextTurn = true;
|
delayedNextTurn = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -509,9 +509,6 @@ void loop() {
|
|||||||
while (buttonEventManager.consumeEvent(ev)) {
|
while (buttonEventManager.consumeEvent(ev)) {
|
||||||
const uint8_t action = actionFor(ev);
|
const uint8_t action = actionFor(ev);
|
||||||
if (action == BA::BTN_DEFAULT) {
|
if (action == BA::BTN_DEFAULT) {
|
||||||
if (ev.type == ButtonEventManager::PressType::Double) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
defaultEvents.push_back(ev);
|
defaultEvents.push_back(ev);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ bool parseSingleBookEntry(OpdsEntry& entryOut, const char* href, const char* typ
|
|||||||
</entry>
|
</entry>
|
||||||
</feed>)";
|
</feed>)";
|
||||||
|
|
||||||
|
std::vector<OpdsEntry> entries;
|
||||||
OpdsParser parser;
|
OpdsParser parser;
|
||||||
|
parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); };
|
||||||
parser.write(reinterpret_cast<const uint8_t*>(xml.data()), xml.size());
|
parser.write(reinterpret_cast<const uint8_t*>(xml.data()), xml.size());
|
||||||
parser.flush();
|
parser.flush();
|
||||||
|
|
||||||
@@ -60,7 +62,6 @@ bool parseSingleBookEntry(OpdsEntry& entryOut, const char* href, const char* typ
|
|||||||
testsFailed++;
|
testsFailed++;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const auto& entries = parser.getEntries();
|
|
||||||
if (entries.size() != 1) {
|
if (entries.size() != 1) {
|
||||||
fprintf(stderr, " FAIL: %s:%d: entries.size() == %zu, expected 1\n", __FILE__, __LINE__, entries.size());
|
fprintf(stderr, " FAIL: %s:%d: entries.size() == %zu, expected 1\n", __FILE__, __LINE__, entries.size());
|
||||||
testsFailed++;
|
testsFailed++;
|
||||||
@@ -153,12 +154,13 @@ void testDistinctAcquisitionFormatsRemainSeparate() {
|
|||||||
</entry>
|
</entry>
|
||||||
</feed>)";
|
</feed>)";
|
||||||
|
|
||||||
|
std::vector<OpdsEntry> entries;
|
||||||
OpdsParser parser;
|
OpdsParser parser;
|
||||||
|
parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); };
|
||||||
parser.write(reinterpret_cast<const uint8_t*>(xml), strlen(xml));
|
parser.write(reinterpret_cast<const uint8_t*>(xml), strlen(xml));
|
||||||
parser.flush();
|
parser.flush();
|
||||||
|
|
||||||
ASSERT_TRUE(!parser.error());
|
ASSERT_TRUE(!parser.error());
|
||||||
const auto& entries = parser.getEntries();
|
|
||||||
ASSERT_SIZE(entries.size(), 1);
|
ASSERT_SIZE(entries.size(), 1);
|
||||||
const auto& links = entries.front().acquisitionLinks;
|
const auto& links = entries.front().acquisitionLinks;
|
||||||
ASSERT_SIZE(links.size(), 4);
|
ASSERT_SIZE(links.size(), 4);
|
||||||
@@ -181,12 +183,14 @@ void testUnsupportedMimeType() {
|
|||||||
</entry>
|
</entry>
|
||||||
</feed>)";
|
</feed>)";
|
||||||
|
|
||||||
|
std::vector<OpdsEntry> entries;
|
||||||
OpdsParser parser;
|
OpdsParser parser;
|
||||||
|
parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); };
|
||||||
parser.write(reinterpret_cast<const uint8_t*>(xml), strlen(xml));
|
parser.write(reinterpret_cast<const uint8_t*>(xml), strlen(xml));
|
||||||
parser.flush();
|
parser.flush();
|
||||||
|
|
||||||
ASSERT_TRUE(!parser.error());
|
ASSERT_TRUE(!parser.error());
|
||||||
ASSERT_SIZE(parser.getEntries().size(), 0);
|
ASSERT_SIZE(entries.size(), 0);
|
||||||
PASS();
|
PASS();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,12 +224,13 @@ void testEmptyHrefOrType() {
|
|||||||
</entry>
|
</entry>
|
||||||
</feed>)";
|
</feed>)";
|
||||||
|
|
||||||
|
std::vector<OpdsEntry> entries;
|
||||||
OpdsParser parser;
|
OpdsParser parser;
|
||||||
|
parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); };
|
||||||
parser.write(reinterpret_cast<const uint8_t*>(xml), strlen(xml));
|
parser.write(reinterpret_cast<const uint8_t*>(xml), strlen(xml));
|
||||||
parser.flush();
|
parser.flush();
|
||||||
|
|
||||||
ASSERT_TRUE(!parser.error());
|
ASSERT_TRUE(!parser.error());
|
||||||
const auto& entries = parser.getEntries();
|
|
||||||
ASSERT_SIZE(entries.size(), 0);
|
ASSERT_SIZE(entries.size(), 0);
|
||||||
PASS();
|
PASS();
|
||||||
}
|
}
|
||||||
@@ -243,12 +248,13 @@ void testDuplicateAcquisitionLinks() {
|
|||||||
</entry>
|
</entry>
|
||||||
</feed>)";
|
</feed>)";
|
||||||
|
|
||||||
|
std::vector<OpdsEntry> entries;
|
||||||
OpdsParser parser;
|
OpdsParser parser;
|
||||||
|
parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); };
|
||||||
parser.write(reinterpret_cast<const uint8_t*>(xml), strlen(xml));
|
parser.write(reinterpret_cast<const uint8_t*>(xml), strlen(xml));
|
||||||
parser.flush();
|
parser.flush();
|
||||||
|
|
||||||
ASSERT_TRUE(!parser.error());
|
ASSERT_TRUE(!parser.error());
|
||||||
const auto& entries = parser.getEntries();
|
|
||||||
ASSERT_SIZE(entries.size(), 1);
|
ASSERT_SIZE(entries.size(), 1);
|
||||||
const auto& links = entries.front().acquisitionLinks;
|
const auto& links = entries.front().acquisitionLinks;
|
||||||
ASSERT_SIZE(links.size(), 2);
|
ASSERT_SIZE(links.size(), 2);
|
||||||
@@ -272,12 +278,13 @@ void testIdenticalHrefAcquisitionLinksAreDeduplicated() {
|
|||||||
</entry>
|
</entry>
|
||||||
</feed>)";
|
</feed>)";
|
||||||
|
|
||||||
|
std::vector<OpdsEntry> entries;
|
||||||
OpdsParser parser;
|
OpdsParser parser;
|
||||||
|
parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); };
|
||||||
parser.write(reinterpret_cast<const uint8_t*>(xml), strlen(xml));
|
parser.write(reinterpret_cast<const uint8_t*>(xml), strlen(xml));
|
||||||
parser.flush();
|
parser.flush();
|
||||||
|
|
||||||
ASSERT_TRUE(!parser.error());
|
ASSERT_TRUE(!parser.error());
|
||||||
const auto& entries = parser.getEntries();
|
|
||||||
ASSERT_SIZE(entries.size(), 1);
|
ASSERT_SIZE(entries.size(), 1);
|
||||||
const auto& links = entries.front().acquisitionLinks;
|
const auto& links = entries.front().acquisitionLinks;
|
||||||
ASSERT_SIZE(links.size(), 1);
|
ASSERT_SIZE(links.size(), 1);
|
||||||
@@ -299,12 +306,13 @@ void testSlashVariantHrefAcquisitionLinksAreDeduplicated() {
|
|||||||
</entry>
|
</entry>
|
||||||
</feed>)";
|
</feed>)";
|
||||||
|
|
||||||
|
std::vector<OpdsEntry> entries;
|
||||||
OpdsParser parser;
|
OpdsParser parser;
|
||||||
|
parser.onEntryParsed = [&](OpdsEntry e) { entries.push_back(std::move(e)); };
|
||||||
parser.write(reinterpret_cast<const uint8_t*>(xml), strlen(xml));
|
parser.write(reinterpret_cast<const uint8_t*>(xml), strlen(xml));
|
||||||
parser.flush();
|
parser.flush();
|
||||||
|
|
||||||
ASSERT_TRUE(!parser.error());
|
ASSERT_TRUE(!parser.error());
|
||||||
const auto& entries = parser.getEntries();
|
|
||||||
ASSERT_SIZE(entries.size(), 1);
|
ASSERT_SIZE(entries.size(), 1);
|
||||||
const auto& links = entries.front().acquisitionLinks;
|
const auto& links = entries.front().acquisitionLinks;
|
||||||
ASSERT_SIZE(links.size(), 1);
|
ASSERT_SIZE(links.size(), 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user