Step 4: enclosure downloads
Streaming download with progress, content sniffing that replaces the long-dead detectFileType(), HTML-body rejection, and placement by rename from an incomplete dir on the same filesystem. Filters are applied once at discovery and recorded in enclosures.state, so the download queue is the table rather than the parse result and max_new_per_check defers work instead of dropping it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RPyeapneuXrCdojsaiXGbe
This commit is contained in:
48
src/db.rs
48
src/db.rs
@@ -227,6 +227,28 @@ impl Db {
|
||||
|
||||
/// Returns true when this enclosure URL is new. False means we have downloaded it
|
||||
/// before, or deliberately reaped it -- either way it is not fetched again.
|
||||
|
||||
pub fn mark_downloaded(&self, url: &str, path: &std::path::Path, bytes: u64) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"UPDATE enclosures SET state = 'done', path = ?2, bytes_done = ?3,
|
||||
downloaded_at = ?4, last_error = NULL WHERE url = ?1",
|
||||
rusqlite::params![url, path.to_string_lossy(), bytes as i64, now()],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The row stays -- a failed URL is still a URL we have seen. `state` says why it has
|
||||
/// no file, and a retry is an explicit act rather than something a rescan does silently.
|
||||
pub fn mark_enclosure(&self, url: &str, state: &str, error: Option<&str>) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"UPDATE enclosures SET state = ?2, last_error = ?3 WHERE url = ?1",
|
||||
rusqlite::params![url, state, error],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn record_enclosure(
|
||||
&self,
|
||||
feed_id: &str,
|
||||
@@ -243,6 +265,32 @@ impl Db {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// An enclosure waiting to be downloaded.
|
||||
#[derive(Debug)]
|
||||
pub struct Pending {
|
||||
pub url: String,
|
||||
pub mime: Option<String>,
|
||||
}
|
||||
|
||||
impl Db {
|
||||
/// The download queue is the table, not the parse result: an enclosure held back by
|
||||
/// `max_new_per_check` is simply picked up by the next scan, in feed order.
|
||||
pub fn pending(&self, feed_id: &str, limit: usize) -> Result<Vec<Pending>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT url, mime FROM enclosures
|
||||
WHERE feed_id = ?1 AND state = 'pending' ORDER BY id LIMIT ?2",
|
||||
)?;
|
||||
let rows = stmt
|
||||
.query_map(rusqlite::params![feed_id, limit as i64], |r| {
|
||||
Ok(Pending { url: r.get(0)?, mime: r.get(1)? })
|
||||
})?
|
||||
.collect::<rusqlite::Result<Vec<_>>>()?;
|
||||
Ok(rows)
|
||||
}
|
||||
}
|
||||
|
||||
/// Unix seconds. Everything time-shaped in the DB is stored this way.
|
||||
pub fn now() -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
|
||||
Reference in New Issue
Block a user