Skip to content

Calibre Web Epub Downloading + Calibre Wireless Device Syncing#219

Merged
daveallie merged 18 commits intocrosspoint-reader:masterfrom
itsthisjustin:feature/calibre-web-and-margins
Jan 7, 2026
Merged

Calibre Web Epub Downloading + Calibre Wireless Device Syncing#219
daveallie merged 18 commits intocrosspoint-reader:masterfrom
itsthisjustin:feature/calibre-web-and-margins

Conversation

@itsthisjustin
Copy link
Contributor

Summary

Adds support for browsing and downloading books from a Calibre-web server via OPDS.

How it works

  1. Configure server URL in Settings → Calibre Web URL (e.g., https://myserver.com:port I use Cloudflare tunnel to make my server accessible anywhere fwiw)
  2. "Calibre Library" will now show on the the home screen
  3. Browse the catalog - navigate through categories like "By Newest", "By Author", "By Series", etc.
  4. Download books - select a book and press Confirm to download the EPUB to your device

Navigation

  • Up/Down - Move through entries
  • Confirm - Open folder or download book
  • Back - Go to parent catalog, or exit to home if at root
  • Navigation entries show with > prefix, books show title and author
  • Button hints update dynamically ("Open" for folders, "Download" for books)

Technical details

  • Fetches OPDS catalog from {server_url}/opds
  • Parses both navigation feeds (catalog links) and acquisition feeds (downloadable books)
  • Maintains navigation history stack for back navigation
  • Handles absolute paths in OPDS links correctly (e.g., /books/opds/navcatalog/...)
  • Downloads EPUBs directly to the SD card root

Note

The server URL should be typed to include https:// if the server requires it - HTTP→HTTPS redirects may cause SSL errors on ESP32.

Additional Context

  • I also changed the home titles to use uppercase for each word and added a setting to change the size of the side margins

@itsthisjustin
Copy link
Contributor Author

Oh and this can be tested if anyone wants to by putting https://home.jmitch.com/books as the calibre web url

@itsthisjustin itsthisjustin changed the title Access Calibre Web and download epubs direct to device Calibre Web Epub Downloading + Calibre Wireless Device Syncing Jan 3, 2026
@daveallie
Copy link
Member

OPDS is a huge add, thanks for this @itsthisjustin, given the size of the PR, I'll need to take a little bit to review this, but excited for it!

@itsthisjustin
Copy link
Contributor Author

Totally understand. Also added Calibre Wireless Device syncing to this as well so you can auto discover Calibre servers and send to device from there. Working on koreader sync in another branch now

Comment on lines +190 to +202
int CrossPointSettings::getReaderSideMargin() const {
switch (sideMargin) {
case MARGIN_NONE:
return 0;
case MARGIN_SMALL:
default:
return 5;
case MARGIN_MEDIUM:
return 20;
case MARGIN_LARGE:
return 30;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would love to keep this split out to avoid merge conflicts with other work, and to not over inflate large PRs

Comment on lines +16 to +55
// Prepend http:// if no protocol specified (server will redirect to https if needed)
std::string ensureProtocol(const std::string& url) {
if (url.find("://") == std::string::npos) {
return "http://" + url;
}
return url;
}

// Extract host with protocol from URL (e.g., "http://example.com" from "http://example.com/path")
std::string extractHost(const std::string& url) {
const size_t protocolEnd = url.find("://");
if (protocolEnd == std::string::npos) {
// No protocol, find first slash
const size_t firstSlash = url.find('/');
return firstSlash == std::string::npos ? url : url.substr(0, firstSlash);
}
// Find the first slash after the protocol
const size_t hostStart = protocolEnd + 3;
const size_t pathStart = url.find('/', hostStart);
return pathStart == std::string::npos ? url : url.substr(0, pathStart);
}

// Build full URL from server URL and path
// If path starts with /, it's an absolute path from the host root
// Otherwise, it's relative to the server URL
std::string buildUrl(const std::string& serverUrl, const std::string& path) {
const std::string urlWithProtocol = ensureProtocol(serverUrl);
if (path.empty()) {
return urlWithProtocol;
}
if (path[0] == '/') {
// Absolute path - use just the host
return extractHost(urlWithProtocol) + path;
}
// Relative path - append to server URL
if (urlWithProtocol.back() == '/') {
return urlWithProtocol + path;
}
return urlWithProtocol + "/" + path;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These have utility outside of this activity, and avoid bloating this activity lets move them into their own UrlUtils (or something similarly named) class

std::string downloadUrl = buildUrl(SETTINGS.opdsServerUrl, book.href);

// Create sanitized filename
std::string filename = "/" + sanitizeFilename(book.title) + ".epub";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we please make the downloaded file name be <title> - <author>.epub

Comment on lines +292 to +298
std::vector<const char*> menuItems;
menuItems.push_back("Browse Files");
if (hasBrowserUrl) {
menuItems.push_back("Calibre Library");
}
menuItems.push_back("File Transfer");
menuItems.push_back("Settings");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: slightly cleaner way here might be instantiate the vector with the fixed three options and insert the fourth

  std::vector<const char*> menuItems = { "Browse Files", "File Transfer", "Settings" };
  if (hasBrowserUrl) {
    // Insert calibre library to be after browse files
    menuItems.insert(menuItems.begin() + 1, "Calibre Library");
  }

int idx = 0;
const int continueIdx = hasContinueReading ? idx++ : -1;
const int browseFilesIdx = idx++;
const int browseBookIdx = hasBrowserUrl ? idx++ : -1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can you please update these variables to be more reflective of their relationship to the OPDS browser, they are a bit confusing at the moment: hasOdpsUrl, browseOdpsFilesIdx, onOdpsBrowserOpen

src/main.cpp Outdated
Comment on lines +234 to +248
if (strlen(SETTINGS.opdsServerUrl) == 0) {
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInputManager, "Calibre Web URL", "", 10, 127, false,
[](const std::string& url) {
strncpy(SETTINGS.opdsServerUrl, url.c_str(), sizeof(SETTINGS.opdsServerUrl) - 1);
SETTINGS.opdsServerUrl[sizeof(SETTINGS.opdsServerUrl) - 1] = '\0';
SETTINGS.saveToFile();
exitActivity();
enterNewActivity(new OpdsBookBrowserActivity(renderer, mappedInputManager, onGoHome));
},
[] {
exitActivity();
onGoHome();
}));
} else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's possible for this branch to ever be hit as HomeActivity validates that the value is set before calling this. It would simplify this file to remove it. Additionally, OpdsBookBrowserActivity already gracefully handles a missing SETTINGS.opdsServerUrl, so there's no risk of a crash or anything.

src/main.cpp Outdated
Comment on lines +255 to +267
// Check WiFi connectivity first
if (WiFi.status() != WL_CONNECTED) {
enterNewActivity(new WifiSelectionActivity(renderer, mappedInputManager, [](bool connected) {
exitActivity();
if (connected) {
launchBrowserWithUrlCheck();
} else {
onGoHome();
}
}));
} else {
launchBrowserWithUrlCheck();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it even possible for the wifi to be connected before this function is called?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, WifiSelectionActivity is not responsible for turning on and off the Wifi chip, even though it does a mode set when starting the scan (which by proxy turns on the wifi). Critically, the changes here never turn off the wifi when existing the OPDS brower, resulting in the Wifi being left on.

I would love all this existing logic (if necessary) and the Wifi control logic to be wrapped up into the activity (or an activity which launches the OpdsBookBrowserActivity). main.cpp should not need to know that OpdsBookBrowserActivity requires an active Wifi connection or that SETTINGS.opdsServerUrl is set, that should be a responsibility of the activity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it even possible for the wifi to be connected before this function is called?

Soooo not now? But I was thinking in the future it might. (especially now that I have KOReader Sync working, it might be a thing that the user sets the device to connect to wifi at some interval to sync on their behalf)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It just becomes a bit messy understanding the responsibility for enabling/disabling Wifi.

Given the battery drain of using wifi, I would love for the functions using it to be wrapped within blocks enabling and disabling it.

Comment on lines +50 to +52
static constexpr uint16_t UDP_PORTS[] = {54982, 48123, 39001, 44044, 59678};
static constexpr size_t UDP_PORT_COUNT = 5;
static constexpr uint16_t LOCAL_UDP_PORT = 8134; // Port to receive responses
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these are only needed as static private variables, please move them into an anonymous namespace in the CPP file. Additionally, if UDP_PORTS is statically assigned, you should just be able to use std::size(UDP_PORTS) in place of UDP_PORT_COUNT

Comment on lines +74 to +94
// Calibre protocol opcodes (from calibre/devices/smart_device_app/driver.py)
static constexpr int OP_OK = 0;
static constexpr int OP_SET_CALIBRE_DEVICE_INFO = 1;
static constexpr int OP_SET_CALIBRE_DEVICE_NAME = 2;
static constexpr int OP_GET_DEVICE_INFORMATION = 3;
static constexpr int OP_TOTAL_SPACE = 4;
static constexpr int OP_FREE_SPACE = 5;
static constexpr int OP_GET_BOOK_COUNT = 6;
static constexpr int OP_SEND_BOOKLISTS = 7;
static constexpr int OP_SEND_BOOK = 8;
static constexpr int OP_GET_INITIALIZATION_INFO = 9;
static constexpr int OP_BOOK_DONE = 11;
static constexpr int OP_NOOP = 12; // Was incorrectly 18
static constexpr int OP_DELETE_BOOK = 13;
static constexpr int OP_GET_BOOK_FILE_SEGMENT = 14;
static constexpr int OP_GET_BOOK_METADATA = 15;
static constexpr int OP_SEND_BOOK_METADATA = 16;
static constexpr int OP_DISPLAY_MESSAGE = 17;
static constexpr int OP_CALIBRE_BUSY = 18;
static constexpr int OP_SET_LIBRARY_INFO = 19;
static constexpr int OP_ERROR = 20;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make more sense to declare these as an enum mapping to uint8_t values?

{"Justify", "Left", "Center", "Right"}},
{"Reader Side Margin", SettingType::ENUM, &CrossPointSettings::sideMargin, {"None", "Small", "Medium", "Large"}},
{"Calibre Web URL", SettingType::ACTION, nullptr, {}},
{"Calibre Wireless Device", SettingType::TOGGLE, &CrossPointSettings::calibreWirelessEnabled, {}},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this need to be a TOGGLE and not an ACTION? Afaict you're always just disabling/enabling the setting when it's selected and the sub activity is entered / left. Is there any real need for globalisation or persistence of this value?

Comment on lines +485 to +486
// Report 10GB free space
sendJsonResponse(OP_OK, "{\"free_space_on_device\":10737418240}");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a future TODO here so we can update it with the actual card free space in the future?


// Draw progress if receiving
if (state == CalibreWirelessState::RECEIVING && currentFileSize > 0) {
const int percent = static_cast<int>((bytesReceived * 100) / currentFileSize);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will overflow for books over ~20.5MB. Probably the safest way around this is:

static_cast<int>(static_cast<float>(bytesReceived) / static_cast<float>(currentFileSize) * 100f);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any idea what the average size of an epub that people are putting on this device? Curious as I was also trying to figure out a safe number for the free space

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10GB is totally fine, just doesn't protect against a full SD card in the future. Most of my EPUBs are only a few MB, but there are some chonkers, like Wind and Truth is 23.4MB, which would overflow here.

Comment on lines +689 to +700
// Progress bar
const int barWidth = pageWidth - 100;
const int barHeight = 20;
const int barX = 50;
const int barY = statusY + 20;

renderer.drawRect(barX, barY, barWidth, barHeight);
renderer.fillRect(barX + 2, barY + 2, (barWidth - 4) * percent / 100, barHeight - 4);

// Percentage text
const std::string percentText = std::to_string(percent) + "%";
renderer.drawCenteredText(UI_10_FONT_ID, barY + barHeight + 15, percentText.c_str());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that you've done a progress bar here but, not for the OPDS downloader. GIven its reuse here, in the OTA downloader, and in the OPDS downloader, let's extract it out to a new function in ScreenComponents and allow it to take x, y, width, height, percent

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep good call out. I did this after the fact and it SHOULD go here as well

Comment on lines +715 to +738
std::string CalibreWirelessActivity::sanitizeFilename(const std::string& name) const {
std::string result;
result.reserve(name.size());

for (char c : name) {
if (c == '/' || c == '\\' || c == ':' || c == '*' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|') {
result += '_';
} else if (c >= 32 && c < 127) {
result += c;
}
}

// Trim leading/trailing spaces and dots
size_t start = 0;
while (start < result.size() && (result[start] == ' ' || result[start] == '.')) {
start++;
}
size_t end = result.size();
while (end > start && (result[end - 1] == ' ' || result[end - 1] == '.')) {
end--;
}

return result.substr(start, end - start);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GIven the reuse, it would be good to split this out into some common string utils

@itsthisjustin
Copy link
Contributor Author

Side note @daveallie I found that switching between my two branches soft bricked my device as it had settings from one flash that didn't exist in the next so I'm curious if we should add a failsafe to reset the settings file if this happens so that at least the device boots

…eb-and-margins

# Conflicts:
#	src/CrossPointSettings.cpp
#	src/CrossPointSettings.h
#	src/activities/settings/SettingsActivity.cpp
@daveallie
Copy link
Member

I'll resist looking back through every commit/comment in the PR, please rerequest my review via GH's review system or by comment once it's good for another look

@itsthisjustin
Copy link
Contributor Author

Item Status
Split out margin settings ✅ Removed
UrlUtils class ✅ src/util/UrlUtils.h
Download filename <title> - .epub ✅ Done
Menu vector insert() ✅ Done
Opds variable naming ✅ hasOpdsUrl, onOpdsBrowserOpen
Remove dead branch in main.cpp ✅ Simplified
Activity handles WiFi ✅ CHECK_WIFI state
UDP_PORTS anonymous namespace ✅ With range-based for loop
Spec source documentation ✅ URL in header comment
TOGGLE → ACTION for Calibre ✅ CalibreSettingsActivity
stateMutex before task delete ✅ Line 77-78
String insert for packet ✅ Line 390
Stub opcodes comments ✅ Lines 424-434
ccVersionNumber 212 comment ✅ Line 466
coverHeight 800 ✅ Line 469-470
CROSSPOINT_VERSION ✅ Lines 488, 491
TODO for free space ✅ Line 498
Unknown opcode logging ✅ Line 441
Overflow fix (64-bit) ✅ ScreenComponents.cpp line 51
Progress bar in ScreenComponents ✅ drawProgressBar()
StringUtils ✅ src/util/StringUtils.h

@itsthisjustin
Copy link
Contributor Author

itsthisjustin commented Jan 4, 2026

Only thing I want to make sure you saw was that while you were reviewing I changed the Calibre settings into a submenu system to avoid an ever growing settings list. I'd recommend eventually we do this to other settings like "Reader Preferences", "Device Sleep Settings" or whatever we can group into submenus 2976113

@daveallie
Copy link
Member

Yeah definitely on board with this change, was mentioning it elsewhere, happy for this to be the first group to move.

Copy link
Member

@daveallie daveallie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few more bits, plus some comments in CalibreWirelessActivity which need addressing/replies

src/main.cpp Outdated
#include "activities/browser/OpdsBookBrowserActivity.h"
#include "activities/home/HomeActivity.h"
#include "activities/network/CrossPointWebServerActivity.h"
#include "activities/network/WifiSelectionActivity.h"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this can be removed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need Opds because it's what allows the onGoToBrowser() to work

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onGoToBrowser just calls enterNewActivity(new OpdsBookBrowserActivity(renderer, mappedInputManager, onGoHome)); which has no dependency on WifiSelectionActivity?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll double check but the opds browser doesnt technically launch until the wifi selection activity shows. It's a prereq for showing the browser that they must connect to a wifi network first.

src/main.cpp Outdated
#include "activities/reader/ReaderActivity.h"
#include "activities/settings/SettingsActivity.h"
#include "activities/util/FullScreenMessageActivity.h"
#include "activities/util/KeyboardEntryActivity.h"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this can be removed

src/main.cpp Outdated
#include <InputManager.h>
#include <SDCardManager.h>
#include <SPI.h>
#include <WiFi.h>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this can be removed

@DJPoulter
Copy link
Contributor

Any chance you can add the option for username and password for opds. To allow access to Calibre-web Automated.

@itsthisjustin
Copy link
Contributor Author

Any chance you can add the option for username and password for opds. To allow access to Calibre-web Automated.

Totally, but I'd say let's get this merged first so it's a smaller PR later to add that. I can add it this week

@daveallie
Copy link
Member

Any chance you can add the option for username and password for opds. To allow access to Calibre-web Automated.

If it's setup anything like my personal setup, you can use HTTP basic auth in the URL to do username and password, so instead of http://host.to.your.opds.server.com, you use http://yourusername:[email protected]

Resolves conflicts in CrossPointSettings.cpp and SettingsActivity.cpp:
- Combines opdsServerUrl (feature branch) with screenMargin and
  sleepScreenCoverMode (master) in settings persistence
- Updates settings count to 16 persisted fields
- Merges settings UI to include all new settings plus Calibre Settings action
- Adopts new SettingInfo factory pattern from master
@itsthisjustin
Copy link
Contributor Author

Would love your thoughts on these ones before merging:

Also looks like there's a small formatting issue which can be fixed by running the format script, but other than that, this looks good!

I think all of these are done no?

itsthisjustin and others added 2 commits January 5, 2026 11:56
I wasn't 100% sure this wasn't left over from me but gitblame says it came from someone else. Either way I don't see anywhere it's used so I THINK it's dead code
@daveallie daveallie merged commit b792b79 into crosspoint-reader:master Jan 7, 2026
1 check passed
@StuckInAWell
Copy link

Hey there guys. Thrilled by this development, now I'm just hoping i can figure out why it's not working for me.

I haven't used Calibre much, but it seems like I set it up right...

I've set up Calibre on my homeserver, and set up the webserver and wireless connection settings.

I also added it to my DDNS to have it accessible from outside. This much has been tested and I can see my library from a browser at that url.

From the x4, I set the URL and I can connect as a wireless device. It sees the server by name, and says it's waiting for commands.
20260112_230251

This is where it starts to act up. Even though it says waiting for commands, I have only once seen Calibre on my server show the "devices" menu at the top. Nothing was done differently, but I went to change the setting mentioned in the post, and then it hung on the x4.

Additionally, when trying to browse the Calibre Library from the home screen on the x4, it opens (sometimes with a glitchy double screen), then has me select the wifi, connects, and then errors out.
20260112_230230

This is the point where I'm lost. Other devices can see the library in a browser, but the X4 cant load it. I've verified the URL, and still no dice.

Happy to try anything you suggest.

@itsthisjustin
Copy link
Contributor Author

Hey there guys. Thrilled by this development, now I'm just hoping i can figure out why it's not working for me.

I haven't used Calibre much, but it seems like I set it up right...

I've set up Calibre on my homeserver, and set up the webserver and wireless connection settings.

I also added it to my DDNS to have it accessible from outside. This much has been tested and I can see my library from a browser at that url.

From the x4, I set the URL and I can connect as a wireless device. It sees the server by name, and says it's waiting for commands. 20260112_230251

This is where it starts to act up. Even though it says waiting for commands, I have only once seen Calibre on my server show the "devices" menu at the top. Nothing was done differently, but I went to change the setting mentioned in the post, and then it hung on the x4.

Additionally, when trying to browse the Calibre Library from the home screen on the x4, it opens (sometimes with a glitchy double screen), then has me select the wifi, connects, and then errors out. 20260112_230230

This is the point where I'm lost. Other devices can see the library in a browser, but the X4 cant load it. I've verified the URL, and still no dice.

Happy to try anything you suggest.

So just to confirm, you are trying to use both the calibre library AND wireless device syncing right? they are two completely different features so I just want to make sure you're not mixing them. For calibre wireless device I just right click the book I want to send and I send to device A or whatever it says. That works. If you are using DDNS with the wireless device thing I'm not 100% sure that will work because it requires UDP commands to work and that might be blocked. You'd want to use this on your local network. I hate to be that guy, but you may want to just make 100% sure you can use the wireless device syncing with another device other than the x4 first just to make sure that's set up correctly and its not some weird internal internet/port issue.

As far as the library issue, I'd probably say the same thing. I tried this with DDNS myself and I could never get it working correctly so I ended up using cloudflare tunnels. Wasn't an issue with the x4 as much as some stupid network thing with ssl and port forwarding so gave up and switched up how I was exposing it.

@StuckInAWell
Copy link

Hey there guys. Thrilled by this development, now I'm just hoping i can figure out why it's not working for me.
I haven't used Calibre much, but it seems like I set it up right...
I've set up Calibre on my homeserver, and set up the webserver and wireless connection settings.
I also added it to my DDNS to have it accessible from outside. This much has been tested and I can see my library from a browser at that url.
From the x4, I set the URL and I can connect as a wireless device. It sees the server by name, and says it's waiting for commands. 20260112_230251
This is where it starts to act up. Even though it says waiting for commands, I have only once seen Calibre on my server show the "devices" menu at the top. Nothing was done differently, but I went to change the setting mentioned in the post, and then it hung on the x4.
Additionally, when trying to browse the Calibre Library from the home screen on the x4, it opens (sometimes with a glitchy double screen), then has me select the wifi, connects, and then errors out. 20260112_230230
This is the point where I'm lost. Other devices can see the library in a browser, but the X4 cant load it. I've verified the URL, and still no dice.
Happy to try anything you suggest.

So just to confirm, you are trying to use both the calibre library AND wireless device syncing right? they are two completely different features so I just want to make sure you're not mixing them. For calibre wireless device I just right click the book I want to send and I send to device A or whatever it says. That works. If you are using DDNS with the wireless device thing I'm not 100% sure that will work because it requires UDP commands to work and that might be blocked. You'd want to use this on your local network. I hate to be that guy, but you may want to just make 100% sure you can use the wireless device syncing with another device other than the x4 first just to make sure that's set up correctly and its not some weird internal internet/port issue.

As far as the library issue, I'd probably say the same thing. I tried this with DDNS myself and I could never get it working correctly so I ended up using cloudflare tunnels. Wasn't an issue with the x4 as much as some stupid network thing with ssl and port forwarding so gave up and switched up how I was exposing it.

Nah, just trying to use one or the other. Wireless device goes over local network, not the DDNS. I did JUST test it with a jailbroken kindle with KOReader and I was able to send a file from Calibre to it.

I can send you the DDNS url privately if you are interested in testing it on your end. I use the same system to expose Jellyfin without a problem.

@itsthisjustin
Copy link
Contributor Author

Hey there guys. Thrilled by this development, now I'm just hoping i can figure out why it's not working for me.
I haven't used Calibre much, but it seems like I set it up right...
I've set up Calibre on my homeserver, and set up the webserver and wireless connection settings.
I also added it to my DDNS to have it accessible from outside. This much has been tested and I can see my library from a browser at that url.
From the x4, I set the URL and I can connect as a wireless device. It sees the server by name, and says it's waiting for commands. 20260112_230251
This is where it starts to act up. Even though it says waiting for commands, I have only once seen Calibre on my server show the "devices" menu at the top. Nothing was done differently, but I went to change the setting mentioned in the post, and then it hung on the x4.
Additionally, when trying to browse the Calibre Library from the home screen on the x4, it opens (sometimes with a glitchy double screen), then has me select the wifi, connects, and then errors out. 20260112_230230
This is the point where I'm lost. Other devices can see the library in a browser, but the X4 cant load it. I've verified the URL, and still no dice.
Happy to try anything you suggest.

So just to confirm, you are trying to use both the calibre library AND wireless device syncing right? they are two completely different features so I just want to make sure you're not mixing them. For calibre wireless device I just right click the book I want to send and I send to device A or whatever it says. That works. If you are using DDNS with the wireless device thing I'm not 100% sure that will work because it requires UDP commands to work and that might be blocked. You'd want to use this on your local network. I hate to be that guy, but you may want to just make 100% sure you can use the wireless device syncing with another device other than the x4 first just to make sure that's set up correctly and its not some weird internal internet/port issue.

As far as the library issue, I'd probably say the same thing. I tried this with DDNS myself and I could never get it working correctly so I ended up using cloudflare tunnels. Wasn't an issue with the x4 as much as some stupid network thing with ssl and port forwarding so gave up and switched up how I was exposing it.

Nah, just trying to use one or the other. Wireless device goes over local network, not the DDNS. I did JUST test it with a jailbroken kindle with KOReader and I was able to send a file from Calibre to it.

I can send you the DDNS url privately if you are interested in testing it on your end. I use the same system to expose Jellyfin without a problem.

Of course. Email me. [email protected]

@itsthisjustin
Copy link
Contributor Author

Hey there guys. Thrilled by this development, now I'm just hoping i can figure out why it's not working for me.

I haven't used Calibre much, but it seems like I set it up right...

I've set up Calibre on my homeserver, and set up the webserver and wireless connection settings.

I also added it to my DDNS to have it accessible from outside. This much has been tested and I can see my library from a browser at that url.

The original feature assumed SSL and failed if you didn't have a cert. PR incoming

@alnav3
Copy link

alnav3 commented Jan 14, 2026

Hey guys! Sorry to bother

I was thinking of buying this ereader and install crosspoint, however one of the non-negotiables im looking at is progress syncing through calibre-web with both the xteink x4 and my kobo libra (it's already working through kobo devices just fine)

As long as i understand, this PR has just added this feature to the device, so in theory if i install crosspoint i can configure the calibre endpoint just as i did with my kobo and sync all books progress through both of them, right?

@itsthisjustin
Copy link
Contributor Author

Hey guys! Sorry to bother

I was thinking of buying this ereader and install crosspoint, however one of the non-negotiables im looking at is progress syncing through calibre-web with both the xteink x4 and my kobo libra (it's already working through kobo devices just fine)

As long as i understand, this PR has just added this feature to the device, so in theory if i install crosspoint i can configure the calibre endpoint just as i did with my kobo and sync all books progress through both of them, right?

This isn't for syncing progress. Do you run KOReader on your kobo?

@itsthisjustin
Copy link
Contributor Author

@daveallie did anything change in main between this working and my merge related to networking? This 100% worked in testing and now is missing kb when a book finishes transfer. I've fixed the disconnect issue but transferring still isn't working and I can't figure out why

yingirene pushed a commit to yingirene/crosspoint-reader that referenced this pull request Jan 16, 2026
…point-reader#219)

Adds support for browsing and downloading books from a Calibre-web
server via OPDS.
How it works
1. Configure server URL in Settings → Calibre Web URL (e.g.,
https://myserver.com:port I use Cloudflare tunnel to make my server
accessible anywhere fwiw)
2. "Calibre Library" will now show on the the home screen
3. Browse the catalog - navigate through categories like "By Newest",
"By Author", "By Series", etc.
4. Download books - select a book and press Confirm to download the EPUB
to your device
Navigation
- Up/Down - Move through entries
- Confirm - Open folder or download book
- Back - Go to parent catalog, or exit to home if at root
- Navigation entries show with > prefix, books show title and author
- Button hints update dynamically ("Open" for folders, "Download" for
books)
Technical details
- Fetches OPDS catalog from {server_url}/opds
- Parses both navigation feeds (catalog links) and acquisition feeds
(downloadable books)
- Maintains navigation history stack for back navigation
- Handles absolute paths in OPDS links correctly (e.g.,
/books/opds/navcatalog/...)
- Downloads EPUBs directly to the SD card root
Note
The server URL should be typed to include https:// if the server
requires it - HTTP→HTTPS redirects may cause SSL errors on ESP32.

* I also changed the home titles to use uppercase for each word and
added a setting to change the size of the side margins

---------

Co-authored-by: Dave Allie <[email protected]>
Unintendedsideeffects pushed a commit to Unintendedsideeffects/crosspoint-reader that referenced this pull request Feb 17, 2026
…point-reader#219)

## Summary

Adds support for browsing and downloading books from a Calibre-web
server via OPDS.
How it works
1. Configure server URL in Settings → Calibre Web URL (e.g.,
https://myserver.com:port I use Cloudflare tunnel to make my server
accessible anywhere fwiw)
2. "Calibre Library" will now show on the the home screen
3. Browse the catalog - navigate through categories like "By Newest",
"By Author", "By Series", etc.
4. Download books - select a book and press Confirm to download the EPUB
to your device
Navigation
- Up/Down - Move through entries
- Confirm - Open folder or download book
- Back - Go to parent catalog, or exit to home if at root
- Navigation entries show with > prefix, books show title and author
- Button hints update dynamically ("Open" for folders, "Download" for
books)
Technical details
- Fetches OPDS catalog from {server_url}/opds
- Parses both navigation feeds (catalog links) and acquisition feeds
(downloadable books)
- Maintains navigation history stack for back navigation
- Handles absolute paths in OPDS links correctly (e.g.,
/books/opds/navcatalog/...)
- Downloads EPUBs directly to the SD card root
Note
The server URL should be typed to include https:// if the server
requires it - HTTP→HTTPS redirects may cause SSL errors on ESP32.

## Additional Context

* I also changed the home titles to use uppercase for each word and
added a setting to change the size of the side margins

---------

Co-authored-by: Dave Allie <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants