Step 5: quota and age retention
Oldest-first reaper with the original's 50 MB headroom pad, plus a reconcile pass for files deleted by hand and pruning of stale entries. The Python meant to reap only read, unflagged episodes but a missing import and a typo made that filter throw on every candidate. Requiring read=1 would be equally dead headless, so flagged is the keep-forever marker and read only decides ordering. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RPyeapneuXrCdojsaiXGbe
This commit is contained in:
32
PROGRESS.md
32
PROGRESS.md
@@ -11,8 +11,7 @@ The full design and step list live in the plan file at
|
||||
- [x] **2. `config.rs` + `db.rs`** — TOML config structs + SQLite schema.
|
||||
- [x] **3. `feed.rs`** — conditional GET, RSS-then-Atom parse, persist entries.
|
||||
- [x] **4. `download.rs`** — downloads, filters, dedupe.
|
||||
- [ ] **5. `retention.rs`** — oldest-first quota + age reaper, `ipx reap [--dry-run]`.
|
||||
*Done when:* smoke 6 passes.
|
||||
- [x] **5. `retention.rs`** — oldest-first quota + age reaper.
|
||||
- [ ] **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.
|
||||
@@ -31,6 +30,35 @@ The full design and step list live in the plan file at
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-09 — Step 5: retention.rs
|
||||
|
||||
`src/retention.rs`: reconcile pass (rows claiming a file that is gone become `reaped`, fixing the
|
||||
step-4 wart), age sweep, quota sweep keeping the original's 50 MB headroom pad, and entry pruning.
|
||||
`ipx reap [--dry-run]`; a sweep also runs before every `fetch`, as the Python did per download.
|
||||
`pick()` and `aged()` are pure so the ordering rules are testable without touching a disk.
|
||||
|
||||
**Judgement call worth Ray's eye.** The Python meant to reap only `read = 1 AND flagged = 0` but
|
||||
never managed it — a missing `plistlib` import and an `EntreiesData` typo made that filter throw on
|
||||
every candidate, so with a `.ipxd` present nothing was ever deleted. Requiring `read = 1` here would
|
||||
be equally dead, because nothing marks episodes read until a UI exists. So: **`flagged` is the
|
||||
keep-forever marker, and `read` only decides what goes first** (`ORDER BY read DESC, downloaded_at
|
||||
ASC`). Quota therefore actually reclaims space headless. Say the word if you would rather unread
|
||||
episodes were never touched.
|
||||
|
||||
Second call: `max_age_days` deletes *files* older than the cutoff, not just fileless entries as the
|
||||
plan's wording had it — "keep 30 days of episodes" is what the setting reads like on a NAS.
|
||||
|
||||
Verified: `cargo test` 21/21, including the two tests encoding the exact bug the Python had —
|
||||
flagged files are never offered, and read sort ahead of unread. Smoke 6 with three 30 MB episodes
|
||||
against a 0.1 GB quota (52.4 MB ceiling after the pad): dry run listed ep1+ep2 and deleted nothing
|
||||
(3 files still on disk), the real run deleted exactly those two oldest, left ep3, flipped both rows
|
||||
to `reaped` with `path = NULL`. A full re-parse with the conditional-GET headers cleared then
|
||||
re-downloaded nothing.
|
||||
|
||||
Next: step 6 — `ipc.rs` + daemon.
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-09 — Step 4: download.rs
|
||||
|
||||
`src/download.rs`: streaming download to `<download_dir>/.ipx-incomplete/` (same filesystem as the
|
||||
|
||||
Reference in New Issue
Block a user