From e2124ca7a0406fb02c64944a0f3f4cb094344da1 Mon Sep 17 00:00:00 2001 From: Justin Mitchell Date: Fri, 16 Jan 2026 18:34:54 -0500 Subject: [PATCH] fixed basic auth for opds and added more calibre commands now supports viewing books on device and deleting them --- USER_GUIDE.md | 13 ++- .../CrossPointReaderCalibrePlugin.zip | Bin 0 -> 8855 bytes .../crosspoint_reader/plugin/Archive.zip | Bin 7816 -> 0 bytes .../crosspoint_reader/plugin/config.py | 5 + .../crosspoint_reader/plugin/driver.py | 101 +++++++++++++++++- src/CrossPointSettings.cpp | 2 +- .../network/CalibreConnectActivity.cpp | 56 +++++----- .../network/CalibreConnectActivity.h | 1 + .../settings/CalibreSettingsActivity.cpp | 4 +- src/network/CrossPointWebServer.cpp | 64 +++++++++++ src/network/CrossPointWebServer.h | 1 + src/network/HttpDownloader.cpp | 9 +- 12 files changed, 220 insertions(+), 36 deletions(-) create mode 100644 calibre-plugin/crosspoint_reader/CrossPointReaderCalibrePlugin.zip delete mode 100644 calibre-plugin/crosspoint_reader/plugin/Archive.zip diff --git a/USER_GUIDE.md b/USER_GUIDE.md index b411140e..edafd05a 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -60,6 +60,17 @@ See the [webserver docs](./docs/webserver.md) for more information on how to con > [!TIP] > Advanced users can also manage files programmatically or via the command line using `curl`. See the [webserver docs](./docs/webserver.md) for details. +### 3.4.1 Calibre Wireless Transfers + +CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin. + +1. Install the plugin in Calibre: + - Open Calibre → Preferences → Plugins → Load plugin from file. + - Select `calibre-plugin/crosspoint_reader/CrossPointReaderCalibrePlugin.zip`. +2. On the device: File Transfer → Connect to Calibre → Join a network. +3. Make sure your computer is on the same WiFi network. +4. In Calibre, click "Send to device" to transfer books. + ### 3.5 Settings The Settings screen allows you to configure the device's behavior. There are a few settings you can adjust: @@ -106,7 +117,7 @@ The Settings screen allows you to configure the device's behavior. There are a f - **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", or "Right". - **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep. - **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting. -- **Calibre Settings**: Set up integration for accessing a Calibre web library or connecting to Calibre as a wireless device. +- **OPDS Browser**: Configure OPDS server settings for browsing and downloading books. Set the server URL (for Calibre Content Server, add `/opds` to the end), and optionally configure username and password for servers requiring authentication. Note: Only HTTP Basic authentication is supported. If using Calibre Content Server with authentication enabled, you must set it to use Basic authentication instead of the default Digest authentication. - **Check for updates**: Check for firmware updates over WiFi. ### 3.6 Sleep Screen diff --git a/calibre-plugin/crosspoint_reader/CrossPointReaderCalibrePlugin.zip b/calibre-plugin/crosspoint_reader/CrossPointReaderCalibrePlugin.zip new file mode 100644 index 0000000000000000000000000000000000000000..61ec171d9f8876c92c7204e50db004a606c299ae GIT binary patch literal 8855 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 diff --git a/calibre-plugin/crosspoint_reader/plugin/config.py b/calibre-plugin/crosspoint_reader/plugin/config.py index 115a57f6..18d4fc94 100644 --- a/calibre-plugin/crosspoint_reader/plugin/config.py +++ b/calibre-plugin/crosspoint_reader/plugin/config.py @@ -22,6 +22,7 @@ PREFS.defaults['port'] = 81 PREFS.defaults['path'] = '/' PREFS.defaults['chunk_size'] = 2048 PREFS.defaults['debug'] = False +PREFS.defaults['fetch_metadata'] = False class CrossPointConfigWidget(QWidget): @@ -35,18 +36,21 @@ class CrossPointConfigWidget(QWidget): self.chunk_size = QSpinBox(self) self.chunk_size.setRange(512, 65536) self.debug = QCheckBox('Enable debug logging', self) + self.fetch_metadata = QCheckBox('Fetch metadata (slower device list)', self) self.host.setText(PREFS['host']) self.port.setValue(PREFS['port']) self.path.setText(PREFS['path']) self.chunk_size.setValue(PREFS['chunk_size']) self.debug.setChecked(PREFS['debug']) + self.fetch_metadata.setChecked(PREFS['fetch_metadata']) layout.addRow('Host', self.host) layout.addRow('Port', self.port) layout.addRow('Upload path', self.path) layout.addRow('Chunk size', self.chunk_size) layout.addRow('', self.debug) + layout.addRow('', self.fetch_metadata) self.log_view = QPlainTextEdit(self) self.log_view.setReadOnly(True) @@ -67,6 +71,7 @@ class CrossPointConfigWidget(QWidget): PREFS['path'] = self.path.text().strip() or PREFS.defaults['path'] PREFS['chunk_size'] = int(self.chunk_size.value()) PREFS['debug'] = bool(self.debug.isChecked()) + PREFS['fetch_metadata'] = bool(self.fetch_metadata.isChecked()) def _refresh_logs(self): self.log_view.setPlainText(get_log_text()) diff --git a/calibre-plugin/crosspoint_reader/plugin/driver.py b/calibre-plugin/crosspoint_reader/plugin/driver.py index 9249846d..846206ff 100644 --- a/calibre-plugin/crosspoint_reader/plugin/driver.py +++ b/calibre-plugin/crosspoint_reader/plugin/driver.py @@ -1,9 +1,13 @@ import os import time +import urllib.parse +import urllib.request from calibre.devices.errors import ControlError from calibre.devices.interface import DevicePlugin from calibre.devices.usbms.deviceconfig import DeviceConfig +from calibre.devices.usbms.books import Book +from calibre.ebooks.metadata.book.base import Metadata from . import ws_client from .config import CrossPointConfigWidget, PREFS @@ -105,6 +109,35 @@ class CrossPointDevice(DeviceConfig, DevicePlugin): else: self.report_progress = report_progress + def _http_base(self): + host = self.device_host or PREFS['host'] + return f'http://{host}' + + def _http_get_json(self, path, params=None, timeout=5): + url = self._http_base() + path + if params: + url += '?' + urllib.parse.urlencode(params) + try: + with urllib.request.urlopen(url, timeout=timeout) as resp: + data = resp.read().decode('utf-8', 'ignore') + except Exception as exc: + raise ControlError(desc=f'HTTP request failed: {exc}') + try: + import json + return json.loads(data) + except Exception as exc: + raise ControlError(desc=f'Invalid JSON response: {exc}') + + def _http_post_form(self, path, data, timeout=5): + url = self._http_base() + path + body = urllib.parse.urlencode(data).encode('utf-8') + req = urllib.request.Request(url, data=body, method='POST') + try: + with urllib.request.urlopen(req, timeout=timeout) as resp: + return resp.status, resp.read().decode('utf-8', 'ignore') + except Exception as exc: + raise ControlError(desc=f'HTTP request failed: {exc}') + def config_widget(self): return CrossPointConfigWidget() @@ -112,8 +145,37 @@ class CrossPointDevice(DeviceConfig, DevicePlugin): config_widget.save() def books(self, oncard=None, end_session=True): - # Device does not expose a browsable library yet. - return [] + if oncard is not None: + return [] + entries = self._http_get_json('/api/files', params={'path': '/'}) + books = [] + fetch_metadata = PREFS['fetch_metadata'] + for entry in entries: + if entry.get('isDirectory'): + continue + if not entry.get('isEpub'): + continue + name = entry.get('name', '') + if not name: + continue + size = entry.get('size', 0) + lpath = '/' + name if not name.startswith('/') else name + title = os.path.splitext(os.path.basename(name))[0] + meta = Metadata(title, []) + if fetch_metadata: + try: + from calibre.customize.ui import quick_metadata + from calibre.ebooks.metadata.meta import get_metadata + with self._download_temp(lpath) as tf: + with quick_metadata: + m = get_metadata(tf, stream_type='epub', force_read_metadata=True) + if m is not None: + meta = m + except Exception as exc: + self._log(f'[CrossPoint] metadata read failed for {lpath}: {exc}') + book = Book('', lpath, size=size, other=meta) + books.append(book) + return books def sync_booklists(self, booklists, end_session=True): # No on-device metadata sync supported. @@ -175,8 +237,39 @@ class CrossPointDevice(DeviceConfig, DevicePlugin): return def delete_books(self, paths, end_session=True): - # Deletion not supported in current device API. - raise ControlError(desc='Device does not support deleting books') + for path in paths: + status, body = self._http_post_form('/delete', {'path': path, 'type': 'file'}) + if status != 200: + raise ControlError(desc=f'Delete failed for {path}: {body}') + + def remove_books_from_metadata(self, paths, booklists): + for path in paths: + for bl in booklists: + for book in tuple(bl): + if path == book.path or path == book.lpath: + bl.remove_book(book) + + def get_file(self, path, outfile, end_session=True, this_book=None, total_books=None): + url = self._http_base() + '/download' + params = urllib.parse.urlencode({'path': path}) + try: + with urllib.request.urlopen(url + '?' + params, timeout=10) as resp: + while True: + chunk = resp.read(65536) + if not chunk: + break + outfile.write(chunk) + except Exception as exc: + raise ControlError(desc=f'Failed to download {path}: {exc}') + + def _download_temp(self, path): + from calibre.ptempfile import PersistentTemporaryFile + tf = PersistentTemporaryFile(suffix='.epub') + self.get_file(path, tf) + tf.flush() + tf.seek(0) + return tf + def eject(self): self.is_connected = False diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 8d599b48..28377eee 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -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 diff --git a/src/activities/network/CalibreConnectActivity.cpp b/src/activities/network/CalibreConnectActivity.cpp index 3c7ef8f9..a096efea 100644 --- a/src/activities/network/CalibreConnectActivity.cpp +++ b/src/activities/network/CalibreConnectActivity.cpp @@ -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) { diff --git a/src/activities/network/CalibreConnectActivity.h b/src/activities/network/CalibreConnectActivity.h index 60d5a70b..08cf4bb4 100644 --- a/src/activities/network/CalibreConnectActivity.h +++ b/src/activities/network/CalibreConnectActivity.h @@ -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(); diff --git a/src/activities/settings/CalibreSettingsActivity.cpp b/src/activities/settings/CalibreSettingsActivity.cpp index 055b9cd1..9e4ce1ca 100644 --- a/src/activities/settings/CalibreSettingsActivity.cpp +++ b/src/activities/settings/CalibreSettingsActivity.cpp @@ -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'; diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index b4796d97..1f5b3ebb 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -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; diff --git a/src/network/CrossPointWebServer.h b/src/network/CrossPointWebServer.h index cff3e05f..36030292 100644 --- a/src/network/CrossPointWebServer.h +++ b/src/network/CrossPointWebServer.h @@ -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; diff --git a/src/network/HttpDownloader.cpp b/src/network/HttpDownloader.cpp index d05eeda3..e7fd4526 100644 --- a/src/network/HttpDownloader.cpp +++ b/src/network/HttpDownloader.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -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();