Serve EPUBs instead XTC in daily OPDS

It turns out it is currently impossible for the Xteink X4 to download
XTC files from an OPDS server. So I switched the built-in OPDS server in
this binary (daily.hallada.net/opds) to serve the EPUB files instead of
the XTC files. The X4 is pretty capable of reading the X4 edition of the
EPUB that was optimized for it anyways, and I prefer the flexibility of
EPUB, so I might end up eventually deleting the XTC conversion. For now,
I kept the conversion step (in case I ever want to ever try manually
copying them to the device) and I keep a limited number of XTC issues on
the server since they are quite big in filesize. The BookOrbit
integration is now optional since the built-in OPDS server is able to
serve the EPUBs. In my installation, I serve the EPUBs through both. The
daily.hallada.net/opds server is just a little quicker to navigate and
download the EPUBs on the X4 since the BookOrbit OPDS requires diving
into a couple layers of folders before you get to the files.
This commit is contained in:
2026-08-15 20:24:20 +00:00
parent 9e30c1dcdf
commit 9e27a32fb6
12 changed files with 630 additions and 282 deletions
+19 -11
View File
@@ -59,7 +59,7 @@ fn test_config(root: &Path) -> Config {
prefilter_keep: 20,
world_briefing: false,
publish: PublishConfig {
bookorbit_dir: root.join("bookorbit"),
epub_dir: root.join("bookorbit"),
xtc_dir: root.join("xtc"),
},
xtc: XtcConfig {
@@ -305,35 +305,44 @@ async fn assemble_build_publish(
);
// --- Publish (§3.11) ---
let published = publish::publish_issue(db, cfg, &issue, &artifacts, None)
let published = publish::publish_issue(cfg, &issue, &artifacts, None)
.await
.expect("publish");
assert_eq!(published.epubs.len(), 2);
for artifact in &published.epubs {
assert!(artifact.path.starts_with(&cfg.publish.bookorbit_dir));
assert!(artifact.path.starts_with(&cfg.publish.epub_dir));
assert!(artifact.path.exists(), "{}", artifact.path.display());
}
assert!(
cfg.publish
.bookorbit_dir
.epub_dir
.join("The Daily EPUB - 2026-08-15.epub")
.exists()
);
assert!(
cfg.publish
.bookorbit_dir
.epub_dir
.join("The Daily EPUB - 2026-08-15 (X4).epub")
.exists()
);
assert!(published.xtc.is_none(), "the converter is disabled here");
// The OPDS feed is regenerated on every publish, even with no XTC files yet.
let opds = published.opds.clone().expect("an OPDS feed was written");
assert_eq!(opds, cfg.publish.xtc_dir.join("xtc.xml"));
let feed = std::fs::read_to_string(&opds).expect("read the OPDS feed");
// The OPDS feed is derived from what was just published — both editions,
// typed so CrossPoint will accept them (§3.11).
let feed = publish::build_opds(db, cfg).await.expect("build the feed");
assert!(feed.starts_with("<?xml"), "{feed}");
assert!(feed.contains("<feed xmlns=\"http://www.w3.org/2005/Atom\""));
assert!(feed.contains(&cfg.server.public_url));
assert_eq!(feed.matches("<entry>").count(), 2, "{feed}");
assert_eq!(feed.matches("application/epub+zip").count(), 2, "{feed}");
assert!(
feed.contains("The Daily EPUB — 2026-08-15</title>"),
"{feed}"
);
assert!(
feed.contains("The Daily EPUB — 2026-08-15 (X4)</title>"),
"{feed}"
);
// --- Record (§3.13) ---
let epub_path = published
@@ -438,7 +447,6 @@ async fn skip_llm_pipeline_produces_a_published_issue() {
// Re-running the same date replaces rather than duplicates (notes §12).
let republished = publish::publish_issue(
&db,
&cfg,
&issue,
&[
@@ -458,7 +466,7 @@ async fn skip_llm_pipeline_produces_a_published_issue() {
.await
.expect("republish");
assert_eq!(republished.epubs.len(), 2);
let files: Vec<String> = std::fs::read_dir(&cfg.publish.bookorbit_dir)
let files: Vec<String> = std::fs::read_dir(&cfg.publish.epub_dir)
.expect("read bookorbit dir")
.filter_map(|e| e.ok().map(|e| e.file_name().to_string_lossy().into_owned()))
.collect();