Step 1: repo skeleton

Rust rewrite of ipodderx-core: cargo project, pinned dependency set,
MIT license carrying the original 2010 copyright, README lineage note,
and PROGRESS.md as the resumable build record.

Dependency tree is rustls-only: librqbit's default features pull in
native-tls and an OpenSSL sha1 backend, so it is built with rust-tls.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RPyeapneuXrCdojsaiXGbe
This commit is contained in:
2026-09-09 19:34:25 +00:00
commit 2c64208b8a
7 changed files with 4326 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

4178
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

22
Cargo.toml Normal file
View File

@@ -0,0 +1,22 @@
[package]
name = "ipx"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.104"
atom_syndication = "0.12.10"
clap = { version = "4.6.6", features = ["derive"] }
dirs = "7.0.0"
infer = "0.22.0"
librqbit = { version = "9.0.1", default-features = false, features = ["rust-tls", "http-api-client"] }
opml = "1.1.6"
reqwest = { version = "0.13.5", default-features = false, features = ["rustls", "http2", "gzip", "stream", "json", "charset", "system-proxy"] }
rss = "2.1.1"
rusqlite = { version = "0.40.2", features = ["bundled"] }
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"
tokio = { version = "1.53.1", features = ["rt-multi-thread", "macros", "fs", "io-util", "net", "sync", "time", "signal"] }
toml = "1.1.5"
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }

25
LICENSE Normal file
View File

@@ -0,0 +1,25 @@
MIT License
Copyright (C) 2010 by Ray Slakinski & August Trometer
Copyright (C) 2026 by Ray Slakinski
This project is a Rust rewrite of ipodderx-core, the Python engine behind
iPodderX (2004-2008), which was released under the MIT License in 2010.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

65
PROGRESS.md Normal file
View File

@@ -0,0 +1,65 @@
# Progress
Running record of what has actually landed. Newest entry first.
The full design and step list live in the plan file at
`/config/.claude/plans/i-want-to-create-playful-quiche.md`.
## Build order
- [x] **1. Repo skeleton** — git init (`main`), `cargo init --name ipx`, deps pinned, LICENSE,
README, this file.
- [ ] **2. `config.rs` + `db.rs`** — TOML config structs + SQLite schema.
*Done when:* `ipx list` prints feeds from a hand-written config; DB created once.
- [ ] **3. `feed.rs`** — conditional GET, RSS-then-Atom parse, persist entries.
*Done when:* `ipx fetch` populates `entries`, second run 304s.
- [ ] **4. `download.rs`** — filename derivation + sanitizer, streaming download, `infer` sniff,
HTML rejection, move into place, dedupe, keyword/explicit filters, `max_new_per_check`.
*Done when:* smoke 1, 2, 4 pass.
- [ ] **5. `retention.rs`** — oldest-first quota + age reaper, `ipx reap [--dry-run]`.
*Done when:* smoke 6 passes.
- [ ] **6. `ipc.rs` + daemon** — broadcast event bus, UDS JSON-lines server, TTL scheduler,
CLI-proxies-to-daemon. *Done when:* smoke 3 passes.
- [ ] **7. `torrent.rs`** — librqbit, seed to ratio/time, stall abort. *Done when:* smoke 5 passes.
- [ ] **8. OPML + polish** — import/export, add/rm/status, tracing setup, systemd unit, README.
## Smoke tests
1. `ipx add <feed>` + `ipx fetch` → file in `download_dir/<Show>/`, row in `enclosures`.
2. `ipx fetch` again → no re-download, feed skipped for TTL.
3. `ipx daemon &` + `nc -U $XDG_RUNTIME_DIR/ipx.sock`, send `{"cmd":"fetch"}` → JSON events;
a concurrent `ipx fetch` proxies to the daemon instead of downloading in parallel.
4. Delete a downloaded file by hand, `ipx fetch` → NOT re-downloaded.
5. Torrent enclosure → downloads, moves, stops seeding at the configured ratio/time.
6. `ipx reap --dry-run` under quota pressure → oldest-first hit list; real run flips rows to
`reaped`.
---
## 2026-09-09 — Step 1: repo skeleton
Repo created at `/src/ipodderx-rs`, default branch `main`, `cargo init --name ipx` (edition 2024,
rustc 1.95.0). LICENSE (MIT, carrying the 2010 copyright), README with the lineage note, and this
file. Hello-world `main.rs` builds.
Dependency versions pinned today — the later steps are written against these APIs:
| crate | version | notes |
|---|---|---|
| tokio | 1.53.1 | rt-multi-thread, macros, fs, io-util, net, sync, time, signal |
| reqwest | 0.13.5 | `default-features = false`; features **`rustls`** (not `rustls-tls` — renamed in 0.13), http2, gzip, stream, json, charset, **`system-proxy`** (env-var proxy pickup is opt-in in 0.13) |
| rss | 2.1.1 | default features; the `with-syndication` feature name in my notes does not exist — Atom is handled by the separate crate |
| atom_syndication | 0.12.10 | |
| rusqlite | 0.40.2 | `bundled` |
| librqbit | 9.0.1 | `default-features = false`; features `rust-tls`, `http-api-client`. The default `default-tls` feature pulls reqwest/native-tls **and** an OpenSSL sha1 backend (`crypto-hash`), which fails to build without pkg-config/OpenSSL headers. API not yet exercised — step 7 |
| serde 1.0.229 / serde_json 1.0.151 / toml 1.1.5 | | |
| clap | 4.6.6 | derive |
| infer 0.22.0 / dirs 7.0.0 / anyhow 1.0.104 / opml 1.1.6 | | |
| tracing 0.1.44 / tracing-subscriber 0.3.23 | | env-filter |
Deferred: nothing.
Gotcha worth keeping: three feature names in the plan were wrong against current crate versions —
`reqwest/rustls-tls` is now `rustls`, env-var proxy support moved behind `system-proxy`, and
`rss/with-syndication` does not exist. `librqbit`'s default features drag in OpenSSL; `rust-tls`
is the fix. Whole tree is rustls-only now, no C TLS dependency.
Next: step 2 — `config.rs` + `db.rs`.

32
README.md Normal file
View File

@@ -0,0 +1,32 @@
# ipodderx-rs
A headless podcatcher: scans RSS/Atom feeds, downloads enclosures (HTTP and BitTorrent),
files them into per-feed folders, and reaps old episodes to stay under a disk quota.
Runs as a one-shot CLI or as a daemon with a Unix-socket JSON event stream for a UI to attach to.
## Lineage
This is a modern Rust rewrite of [ipodderx-core](https://git.sdf1.net/rays/ipodderx-core), the
Python 2 engine behind **iPodderX** (2004-2008, Ray Slakinski & August Trometer), which was
open-sourced under the MIT License in 2010.
What carries over: the feed scan and TTL handling, GUID/URL dedupe, per-feed and per-date download
folders, keyword filters, the explicit-content filter, torrent enclosures, and "SmartSpace" — the
oldest-first disk quota reaper.
What does not: iTunes and iPhoto export via AppleScript, text-to-speech enclosures, the Windows
WMP/COM paths, XML plists and Python pickles for state, the `directory.iPodderX.com` survey ping,
3DES-encrypted preferences, and the `printMSG` stdout protocol (replaced by a JSON-lines socket).
## Status
Under construction. See [PROGRESS.md](PROGRESS.md) for what works today.
## Configuration
`~/.config/ipx/config.toml`, state in `~/.local/share/ipx/state.db`. See PROGRESS.md until the
usage section lands.
## License
MIT. See [LICENSE](LICENSE).

3
src/main.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
println!("ipx {}", env!("CARGO_PKG_VERSION"));
}