fixed basic auth for opds and added more calibre commands

now supports viewing books on device and deleting them
This commit is contained in:
Justin Mitchell
2026-01-16 18:34:54 -05:00
parent 8114899bef
commit e2124ca7a0
12 changed files with 220 additions and 36 deletions

View File

@@ -14,7 +14,7 @@ CrossPointSettings CrossPointSettings::instance;
namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
// Increment this when adding new persisted settings fields
constexpr uint8_t SETTINGS_COUNT = 20;
constexpr uint8_t SETTINGS_COUNT = 21;
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
} // namespace

View File

@@ -33,6 +33,7 @@ void CalibreConnectActivity::onEnter() {
currentUploadName.clear();
lastCompleteName.clear();
lastCompleteAt = 0;
exitRequested = false;
xTaskCreate(&CalibreConnectActivity::taskTrampoline, "CalibreConnectTask",
2048, // Stack size
@@ -124,8 +125,7 @@ void CalibreConnectActivity::loop() {
}
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onComplete();
return;
exitRequested = true;
}
if (webServer && webServer->isRunning()) {
@@ -135,17 +135,17 @@ void CalibreConnectActivity::loop() {
}
esp_task_wdt_reset();
constexpr int MAX_ITERATIONS = 500;
constexpr int MAX_ITERATIONS = 80;
for (int i = 0; i < MAX_ITERATIONS && webServer->isRunning(); i++) {
webServer->handleClient();
if ((i & 0x1F) == 0x1F) {
if ((i & 0x07) == 0x07) {
esp_task_wdt_reset();
}
if ((i & 0x3F) == 0x3F) {
if ((i & 0x0F) == 0x0F) {
yield();
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onComplete();
return;
exitRequested = true;
break;
}
}
}
@@ -181,6 +181,11 @@ void CalibreConnectActivity::loop() {
updateRequired = true;
}
}
if (exitRequested) {
onComplete();
return;
}
}
void CalibreConnectActivity::displayTaskLoop() {
@@ -215,10 +220,14 @@ void CalibreConnectActivity::render() const {
void CalibreConnectActivity::renderServerRunning() const {
constexpr int LINE_SPACING = 24;
constexpr int TOP_PADDING = 18;
constexpr int SMALL_SPACING = 20;
constexpr int SECTION_SPACING = 40;
constexpr int TOP_PADDING = 14;
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Connect to Calibre", true, EpdFontFamily::BOLD);
int y = 60 + TOP_PADDING;
int y = 55 + TOP_PADDING;
renderer.drawCenteredText(UI_10_FONT_ID, y, "Network", true, EpdFontFamily::BOLD);
y += LINE_SPACING;
std::string ssidInfo = "Network: " + connectedSSID;
if (ssidInfo.length() > 28) {
ssidInfo.replace(25, ssidInfo.length() - 25, "...");
@@ -226,22 +235,17 @@ void CalibreConnectActivity::renderServerRunning() const {
renderer.drawCenteredText(UI_10_FONT_ID, y, ssidInfo.c_str());
renderer.drawCenteredText(UI_10_FONT_ID, y + LINE_SPACING, ("IP: " + connectedIP).c_str());
y += LINE_SPACING * 2;
renderer.drawCenteredText(SMALL_FONT_ID, y, "Install the CrossPoint Reader");
renderer.drawCenteredText(SMALL_FONT_ID, y + LINE_SPACING, "device plugin in Calibre.");
y += LINE_SPACING * 2 + SECTION_SPACING;
renderer.drawCenteredText(UI_10_FONT_ID, y, "Setup", true, EpdFontFamily::BOLD);
y += LINE_SPACING;
renderer.drawCenteredText(SMALL_FONT_ID, y, "1) Install CrossPoint Reader plugin");
renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING, "2) Be on the same WiFi network");
renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 2, "3) In Calibre: \"Send to device\"");
renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 3, "Keep this screen open while sending");
y += LINE_SPACING * 2;
renderer.drawCenteredText(SMALL_FONT_ID, y, "Make sure your computer is");
renderer.drawCenteredText(SMALL_FONT_ID, y + LINE_SPACING, "on the same WiFi network.");
y += LINE_SPACING * 2;
renderer.drawCenteredText(SMALL_FONT_ID, y, "Then in Calibre, click");
renderer.drawCenteredText(SMALL_FONT_ID, y + LINE_SPACING, "\"Send to device\".");
y += LINE_SPACING * 2;
renderer.drawCenteredText(SMALL_FONT_ID, y, "Leave this screen open while sending.");
y += LINE_SPACING * 2;
y += SMALL_SPACING * 3 + SECTION_SPACING;
renderer.drawCenteredText(UI_10_FONT_ID, y, "Status", true, EpdFontFamily::BOLD);
y += LINE_SPACING;
if (lastProgressTotal > 0 && lastProgressReceived <= lastProgressTotal) {
std::string label = "Receiving";
if (!currentUploadName.empty()) {
@@ -254,9 +258,9 @@ void CalibreConnectActivity::renderServerRunning() const {
constexpr int barWidth = 300;
constexpr int barHeight = 16;
constexpr int barX = (480 - barWidth) / 2;
ScreenComponents::drawProgressBar(renderer, barX, y + 28, barWidth, barHeight, lastProgressReceived,
ScreenComponents::drawProgressBar(renderer, barX, y + 22, barWidth, barHeight, lastProgressReceived,
lastProgressTotal);
y += 46;
y += 40;
}
if (lastCompleteAt > 0 && (millis() - lastCompleteAt) < 6000) {

View File

@@ -32,6 +32,7 @@ class CalibreConnectActivity final : public ActivityWithSubactivity {
std::string currentUploadName;
std::string lastCompleteName;
unsigned long lastCompleteAt = 0;
bool exitRequested = false;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();

View File

@@ -116,8 +116,8 @@ void CalibreSettingsActivity::handleSelection() {
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "Password", SETTINGS.opdsPassword, 10,
63, // maxLength
true, // password mode
63, // maxLength
false, // not password mode
[this](const std::string& password) {
strncpy(SETTINGS.opdsPassword, password.c_str(), sizeof(SETTINGS.opdsPassword) - 1);
SETTINGS.opdsPassword[sizeof(SETTINGS.opdsPassword) - 1] = '\0';

View File

@@ -90,6 +90,7 @@ void CrossPointWebServer::begin() {
server->on("/api/status", HTTP_GET, [this] { handleStatus(); });
server->on("/api/files", HTTP_GET, [this] { handleFileListData(); });
server->on("/download", HTTP_GET, [this] { handleDownload(); });
// Upload endpoint with special handling for multipart form data
server->on("/upload", HTTP_POST, [this] { handleUploadPost(); }, [this] { handleUpload(); });
@@ -382,6 +383,69 @@ void CrossPointWebServer::handleFileListData() const {
Serial.printf("[%lu] [WEB] Served file listing page for path: %s\n", millis(), currentPath.c_str());
}
void CrossPointWebServer::handleDownload() const {
if (!server->hasArg("path")) {
server->send(400, "text/plain", "Missing path");
return;
}
String itemPath = server->arg("path");
if (itemPath.isEmpty() || itemPath == "/") {
server->send(400, "text/plain", "Invalid path");
return;
}
if (!itemPath.startsWith("/")) {
itemPath = "/" + itemPath;
}
const String itemName = itemPath.substring(itemPath.lastIndexOf('/') + 1);
if (itemName.startsWith(".")) {
server->send(403, "text/plain", "Cannot access system files");
return;
}
for (size_t i = 0; i < HIDDEN_ITEMS_COUNT; i++) {
if (itemName.equals(HIDDEN_ITEMS[i])) {
server->send(403, "text/plain", "Cannot access protected items");
return;
}
}
if (!SdMan.exists(itemPath.c_str())) {
server->send(404, "text/plain", "Item not found");
return;
}
FsFile file = SdMan.open(itemPath.c_str());
if (!file) {
server->send(500, "text/plain", "Failed to open file");
return;
}
if (file.isDirectory()) {
file.close();
server->send(400, "text/plain", "Path is a directory");
return;
}
String contentType = "application/octet-stream";
if (isEpubFile(itemPath)) {
contentType = "application/epub+zip";
}
char nameBuf[128] = {0};
String filename = "download";
if (file.getName(nameBuf, sizeof(nameBuf))) {
filename = nameBuf;
}
server->setContentLength(file.size());
server->sendHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
server->send(200, contentType.c_str(), "");
WiFiClient client = server->client();
client.write(file);
file.close();
}
// Static variables for upload handling
static FsFile uploadFile;
static String uploadFileName;

View File

@@ -73,6 +73,7 @@ class CrossPointWebServer {
void handleStatus() const;
void handleFileList() const;
void handleFileListData() const;
void handleDownload() const;
void handleUpload() const;
void handleUploadPost() const;
void handleCreateFolder() const;

View File

@@ -4,6 +4,7 @@
#include <HardwareSerial.h>
#include <WiFiClient.h>
#include <WiFiClientSecure.h>
#include <base64.h>
#include <cstring>
#include <memory>
@@ -31,7 +32,9 @@ bool HttpDownloader::fetchUrl(const std::string& url, std::string& outContent) {
// Add Basic HTTP auth if credentials are configured
if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
http.setAuthorization(SETTINGS.opdsUsername, SETTINGS.opdsPassword);
std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword;
String encoded = base64::encode(credentials.c_str());
http.addHeader("Authorization", "Basic " + encoded);
}
const int httpCode = http.GET();
@@ -70,7 +73,9 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string&
// Add Basic HTTP auth if credentials are configured
if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
http.setAuthorization(SETTINGS.opdsUsername, SETTINGS.opdsPassword);
std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword;
String encoded = base64::encode(credentials.c_str());
http.addHeader("Authorization", "Basic " + encoded);
}
const int httpCode = http.GET();