Step 2: TOML config and SQLite state

config.rs replaces iPXSettings.py and feeds.plist; db.rs replaces the
per-feed .ipxd plists, history.dat and qmcache.dat. enclosures.url is
UNIQUE, which is the dedupe key the old pickle history provided.

ipx list is the first working subcommand.

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:37:02 +00:00
parent 2c64208b8a
commit a13f19136c
6 changed files with 512 additions and 5 deletions

View File

@@ -1,3 +1,63 @@
fn main() {
println!("ipx {}", env!("CARGO_PKG_VERSION"));
mod config;
mod db;
use anyhow::Result;
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "ipx", version, about = "A headless podcatcher")]
struct Cli {
/// Config file (default: $XDG_CONFIG_HOME/ipx/config.toml)
#[arg(long, global = true)]
config: Option<std::path::PathBuf>,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Show configured feeds and their state
List,
}
fn main() -> Result<()> {
let cli = Cli::parse();
let config_path = cli.config.unwrap_or_else(config::config_path);
let cfg = config::Config::load(&config_path)?;
let db = db::Db::open(&config::data_dir().join("state.db"))?;
match cli.command {
Command::List => list(&cfg, &db),
}
}
fn list(cfg: &config::Config, db: &db::Db) -> Result<()> {
if cfg.feeds.is_empty() {
println!("No feeds configured in {}", config::config_path().display());
return Ok(());
}
for (id, feed) in &cfg.feeds {
let s = db.feed_summary(id)?;
let title = s.title.as_deref().unwrap_or("-");
println!("{id} {title}");
println!(" url {}", feed.url);
println!(" last checked {}", ago(s.last_checked));
println!(" entries {} ({} downloaded)", s.entries, s.downloaded);
if let Some(err) = &s.last_error {
println!(" last error {err}");
}
}
Ok(())
}
fn ago(t: Option<i64>) -> String {
let Some(t) = t else { return "never".into() };
let secs = (db::now() - t).max(0);
match secs {
s if s < 90 => format!("{s}s ago"),
s if s < 5400 => format!("{}m ago", s / 60),
s if s < 172_800 => format!("{}h ago", s / 3600),
s => format!("{}d ago", s / 86_400),
}
}