mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-05 15:17:37 +03:00
fixed basic auth for opds and added more calibre commands
now supports viewing books on device and deleting them
This commit is contained in:
parent
8114899bef
commit
e2124ca7a0
@ -60,6 +60,17 @@ See the [webserver docs](./docs/webserver.md) for more information on how to con
|
|||||||
> [!TIP]
|
> [!TIP]
|
||||||
> Advanced users can also manage files programmatically or via the command line using `curl`. See the [webserver docs](./docs/webserver.md) for details.
|
> 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
|
### 3.5 Settings
|
||||||
|
|
||||||
The Settings screen allows you to configure the device's behavior. There are a few settings you can adjust:
|
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".
|
- **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.
|
- **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.
|
- **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.
|
- **Check for updates**: Check for firmware updates over WiFi.
|
||||||
|
|
||||||
### 3.6 Sleep Screen
|
### 3.6 Sleep Screen
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -22,6 +22,7 @@ PREFS.defaults['port'] = 81
|
|||||||
PREFS.defaults['path'] = '/'
|
PREFS.defaults['path'] = '/'
|
||||||
PREFS.defaults['chunk_size'] = 2048
|
PREFS.defaults['chunk_size'] = 2048
|
||||||
PREFS.defaults['debug'] = False
|
PREFS.defaults['debug'] = False
|
||||||
|
PREFS.defaults['fetch_metadata'] = False
|
||||||
|
|
||||||
|
|
||||||
class CrossPointConfigWidget(QWidget):
|
class CrossPointConfigWidget(QWidget):
|
||||||
@ -35,18 +36,21 @@ class CrossPointConfigWidget(QWidget):
|
|||||||
self.chunk_size = QSpinBox(self)
|
self.chunk_size = QSpinBox(self)
|
||||||
self.chunk_size.setRange(512, 65536)
|
self.chunk_size.setRange(512, 65536)
|
||||||
self.debug = QCheckBox('Enable debug logging', self)
|
self.debug = QCheckBox('Enable debug logging', self)
|
||||||
|
self.fetch_metadata = QCheckBox('Fetch metadata (slower device list)', self)
|
||||||
|
|
||||||
self.host.setText(PREFS['host'])
|
self.host.setText(PREFS['host'])
|
||||||
self.port.setValue(PREFS['port'])
|
self.port.setValue(PREFS['port'])
|
||||||
self.path.setText(PREFS['path'])
|
self.path.setText(PREFS['path'])
|
||||||
self.chunk_size.setValue(PREFS['chunk_size'])
|
self.chunk_size.setValue(PREFS['chunk_size'])
|
||||||
self.debug.setChecked(PREFS['debug'])
|
self.debug.setChecked(PREFS['debug'])
|
||||||
|
self.fetch_metadata.setChecked(PREFS['fetch_metadata'])
|
||||||
|
|
||||||
layout.addRow('Host', self.host)
|
layout.addRow('Host', self.host)
|
||||||
layout.addRow('Port', self.port)
|
layout.addRow('Port', self.port)
|
||||||
layout.addRow('Upload path', self.path)
|
layout.addRow('Upload path', self.path)
|
||||||
layout.addRow('Chunk size', self.chunk_size)
|
layout.addRow('Chunk size', self.chunk_size)
|
||||||
layout.addRow('', self.debug)
|
layout.addRow('', self.debug)
|
||||||
|
layout.addRow('', self.fetch_metadata)
|
||||||
|
|
||||||
self.log_view = QPlainTextEdit(self)
|
self.log_view = QPlainTextEdit(self)
|
||||||
self.log_view.setReadOnly(True)
|
self.log_view.setReadOnly(True)
|
||||||
@ -67,6 +71,7 @@ class CrossPointConfigWidget(QWidget):
|
|||||||
PREFS['path'] = self.path.text().strip() or PREFS.defaults['path']
|
PREFS['path'] = self.path.text().strip() or PREFS.defaults['path']
|
||||||
PREFS['chunk_size'] = int(self.chunk_size.value())
|
PREFS['chunk_size'] = int(self.chunk_size.value())
|
||||||
PREFS['debug'] = bool(self.debug.isChecked())
|
PREFS['debug'] = bool(self.debug.isChecked())
|
||||||
|
PREFS['fetch_metadata'] = bool(self.fetch_metadata.isChecked())
|
||||||
|
|
||||||
def _refresh_logs(self):
|
def _refresh_logs(self):
|
||||||
self.log_view.setPlainText(get_log_text())
|
self.log_view.setPlainText(get_log_text())
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
from calibre.devices.errors import ControlError
|
from calibre.devices.errors import ControlError
|
||||||
from calibre.devices.interface import DevicePlugin
|
from calibre.devices.interface import DevicePlugin
|
||||||
from calibre.devices.usbms.deviceconfig import DeviceConfig
|
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 . import ws_client
|
||||||
from .config import CrossPointConfigWidget, PREFS
|
from .config import CrossPointConfigWidget, PREFS
|
||||||
@ -105,6 +109,35 @@ class CrossPointDevice(DeviceConfig, DevicePlugin):
|
|||||||
else:
|
else:
|
||||||
self.report_progress = report_progress
|
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):
|
def config_widget(self):
|
||||||
return CrossPointConfigWidget()
|
return CrossPointConfigWidget()
|
||||||
|
|
||||||
@ -112,8 +145,37 @@ class CrossPointDevice(DeviceConfig, DevicePlugin):
|
|||||||
config_widget.save()
|
config_widget.save()
|
||||||
|
|
||||||
def books(self, oncard=None, end_session=True):
|
def books(self, oncard=None, end_session=True):
|
||||||
# Device does not expose a browsable library yet.
|
if oncard is not None:
|
||||||
return []
|
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):
|
def sync_booklists(self, booklists, end_session=True):
|
||||||
# No on-device metadata sync supported.
|
# No on-device metadata sync supported.
|
||||||
@ -175,8 +237,39 @@ class CrossPointDevice(DeviceConfig, DevicePlugin):
|
|||||||
return
|
return
|
||||||
|
|
||||||
def delete_books(self, paths, end_session=True):
|
def delete_books(self, paths, end_session=True):
|
||||||
# Deletion not supported in current device API.
|
for path in paths:
|
||||||
raise ControlError(desc='Device does not support deleting books')
|
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):
|
def eject(self):
|
||||||
self.is_connected = False
|
self.is_connected = False
|
||||||
|
|||||||
@ -14,7 +14,7 @@ CrossPointSettings CrossPointSettings::instance;
|
|||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
||||||
// Increment this when adding new persisted settings fields
|
// 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";
|
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|||||||
@ -33,6 +33,7 @@ void CalibreConnectActivity::onEnter() {
|
|||||||
currentUploadName.clear();
|
currentUploadName.clear();
|
||||||
lastCompleteName.clear();
|
lastCompleteName.clear();
|
||||||
lastCompleteAt = 0;
|
lastCompleteAt = 0;
|
||||||
|
exitRequested = false;
|
||||||
|
|
||||||
xTaskCreate(&CalibreConnectActivity::taskTrampoline, "CalibreConnectTask",
|
xTaskCreate(&CalibreConnectActivity::taskTrampoline, "CalibreConnectTask",
|
||||||
2048, // Stack size
|
2048, // Stack size
|
||||||
@ -124,8 +125,7 @@ void CalibreConnectActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
onComplete();
|
exitRequested = true;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (webServer && webServer->isRunning()) {
|
if (webServer && webServer->isRunning()) {
|
||||||
@ -135,17 +135,17 @@ void CalibreConnectActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
esp_task_wdt_reset();
|
esp_task_wdt_reset();
|
||||||
constexpr int MAX_ITERATIONS = 500;
|
constexpr int MAX_ITERATIONS = 80;
|
||||||
for (int i = 0; i < MAX_ITERATIONS && webServer->isRunning(); i++) {
|
for (int i = 0; i < MAX_ITERATIONS && webServer->isRunning(); i++) {
|
||||||
webServer->handleClient();
|
webServer->handleClient();
|
||||||
if ((i & 0x1F) == 0x1F) {
|
if ((i & 0x07) == 0x07) {
|
||||||
esp_task_wdt_reset();
|
esp_task_wdt_reset();
|
||||||
}
|
}
|
||||||
if ((i & 0x3F) == 0x3F) {
|
if ((i & 0x0F) == 0x0F) {
|
||||||
yield();
|
yield();
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
onComplete();
|
exitRequested = true;
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -181,6 +181,11 @@ void CalibreConnectActivity::loop() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (exitRequested) {
|
||||||
|
onComplete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CalibreConnectActivity::displayTaskLoop() {
|
void CalibreConnectActivity::displayTaskLoop() {
|
||||||
@ -215,10 +220,14 @@ void CalibreConnectActivity::render() const {
|
|||||||
|
|
||||||
void CalibreConnectActivity::renderServerRunning() const {
|
void CalibreConnectActivity::renderServerRunning() const {
|
||||||
constexpr int LINE_SPACING = 24;
|
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);
|
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;
|
std::string ssidInfo = "Network: " + connectedSSID;
|
||||||
if (ssidInfo.length() > 28) {
|
if (ssidInfo.length() > 28) {
|
||||||
ssidInfo.replace(25, ssidInfo.length() - 25, "...");
|
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, ssidInfo.c_str());
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, y + LINE_SPACING, ("IP: " + connectedIP).c_str());
|
renderer.drawCenteredText(UI_10_FONT_ID, y + LINE_SPACING, ("IP: " + connectedIP).c_str());
|
||||||
|
|
||||||
y += LINE_SPACING * 2;
|
y += LINE_SPACING * 2 + SECTION_SPACING;
|
||||||
renderer.drawCenteredText(SMALL_FONT_ID, y, "Install the CrossPoint Reader");
|
renderer.drawCenteredText(UI_10_FONT_ID, y, "Setup", true, EpdFontFamily::BOLD);
|
||||||
renderer.drawCenteredText(SMALL_FONT_ID, y + LINE_SPACING, "device plugin in Calibre.");
|
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;
|
y += SMALL_SPACING * 3 + SECTION_SPACING;
|
||||||
renderer.drawCenteredText(SMALL_FONT_ID, y, "Make sure your computer is");
|
renderer.drawCenteredText(UI_10_FONT_ID, y, "Status", true, EpdFontFamily::BOLD);
|
||||||
renderer.drawCenteredText(SMALL_FONT_ID, y + LINE_SPACING, "on the same WiFi network.");
|
y += LINE_SPACING;
|
||||||
|
|
||||||
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;
|
|
||||||
if (lastProgressTotal > 0 && lastProgressReceived <= lastProgressTotal) {
|
if (lastProgressTotal > 0 && lastProgressReceived <= lastProgressTotal) {
|
||||||
std::string label = "Receiving";
|
std::string label = "Receiving";
|
||||||
if (!currentUploadName.empty()) {
|
if (!currentUploadName.empty()) {
|
||||||
@ -254,9 +258,9 @@ void CalibreConnectActivity::renderServerRunning() const {
|
|||||||
constexpr int barWidth = 300;
|
constexpr int barWidth = 300;
|
||||||
constexpr int barHeight = 16;
|
constexpr int barHeight = 16;
|
||||||
constexpr int barX = (480 - barWidth) / 2;
|
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);
|
lastProgressTotal);
|
||||||
y += 46;
|
y += 40;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastCompleteAt > 0 && (millis() - lastCompleteAt) < 6000) {
|
if (lastCompleteAt > 0 && (millis() - lastCompleteAt) < 6000) {
|
||||||
|
|||||||
@ -32,6 +32,7 @@ class CalibreConnectActivity final : public ActivityWithSubactivity {
|
|||||||
std::string currentUploadName;
|
std::string currentUploadName;
|
||||||
std::string lastCompleteName;
|
std::string lastCompleteName;
|
||||||
unsigned long lastCompleteAt = 0;
|
unsigned long lastCompleteAt = 0;
|
||||||
|
bool exitRequested = false;
|
||||||
|
|
||||||
static void taskTrampoline(void* param);
|
static void taskTrampoline(void* param);
|
||||||
[[noreturn]] void displayTaskLoop();
|
[[noreturn]] void displayTaskLoop();
|
||||||
|
|||||||
@ -117,7 +117,7 @@ void CalibreSettingsActivity::handleSelection() {
|
|||||||
enterNewActivity(new KeyboardEntryActivity(
|
enterNewActivity(new KeyboardEntryActivity(
|
||||||
renderer, mappedInput, "Password", SETTINGS.opdsPassword, 10,
|
renderer, mappedInput, "Password", SETTINGS.opdsPassword, 10,
|
||||||
63, // maxLength
|
63, // maxLength
|
||||||
true, // password mode
|
false, // not password mode
|
||||||
[this](const std::string& password) {
|
[this](const std::string& password) {
|
||||||
strncpy(SETTINGS.opdsPassword, password.c_str(), sizeof(SETTINGS.opdsPassword) - 1);
|
strncpy(SETTINGS.opdsPassword, password.c_str(), sizeof(SETTINGS.opdsPassword) - 1);
|
||||||
SETTINGS.opdsPassword[sizeof(SETTINGS.opdsPassword) - 1] = '\0';
|
SETTINGS.opdsPassword[sizeof(SETTINGS.opdsPassword) - 1] = '\0';
|
||||||
|
|||||||
@ -90,6 +90,7 @@ void CrossPointWebServer::begin() {
|
|||||||
|
|
||||||
server->on("/api/status", HTTP_GET, [this] { handleStatus(); });
|
server->on("/api/status", HTTP_GET, [this] { handleStatus(); });
|
||||||
server->on("/api/files", HTTP_GET, [this] { handleFileListData(); });
|
server->on("/api/files", HTTP_GET, [this] { handleFileListData(); });
|
||||||
|
server->on("/download", HTTP_GET, [this] { handleDownload(); });
|
||||||
|
|
||||||
// Upload endpoint with special handling for multipart form data
|
// Upload endpoint with special handling for multipart form data
|
||||||
server->on("/upload", HTTP_POST, [this] { handleUploadPost(); }, [this] { handleUpload(); });
|
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());
|
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 variables for upload handling
|
||||||
static FsFile uploadFile;
|
static FsFile uploadFile;
|
||||||
static String uploadFileName;
|
static String uploadFileName;
|
||||||
|
|||||||
@ -73,6 +73,7 @@ class CrossPointWebServer {
|
|||||||
void handleStatus() const;
|
void handleStatus() const;
|
||||||
void handleFileList() const;
|
void handleFileList() const;
|
||||||
void handleFileListData() const;
|
void handleFileListData() const;
|
||||||
|
void handleDownload() const;
|
||||||
void handleUpload() const;
|
void handleUpload() const;
|
||||||
void handleUploadPost() const;
|
void handleUploadPost() const;
|
||||||
void handleCreateFolder() const;
|
void handleCreateFolder() const;
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <WiFiClient.h>
|
#include <WiFiClient.h>
|
||||||
#include <WiFiClientSecure.h>
|
#include <WiFiClientSecure.h>
|
||||||
|
#include <base64.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@ -31,7 +32,9 @@ bool HttpDownloader::fetchUrl(const std::string& url, std::string& outContent) {
|
|||||||
|
|
||||||
// Add Basic HTTP auth if credentials are configured
|
// Add Basic HTTP auth if credentials are configured
|
||||||
if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
|
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();
|
const int httpCode = http.GET();
|
||||||
@ -70,7 +73,9 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string&
|
|||||||
|
|
||||||
// Add Basic HTTP auth if credentials are configured
|
// Add Basic HTTP auth if credentials are configured
|
||||||
if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) {
|
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();
|
const int httpCode = http.GET();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user