Schedule pickers, and target progress at one row

"Check feeds every" becomes a number plus a unit dropdown in both global
and per-feed settings; parse_interval gained weeks to back it. The
per-feed dropdown can select the global default, clearing the override.

Fixes progress painting every pending row: the event carried no enclosure
id, so the handler had nothing to target and set the width on all of them.
Adding a feed looked like it was downloading everything. Progress,
DownloadDone and DownloadError now carry the enclosure id.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
This commit is contained in:
2026-09-10 02:38:14 +00:00
parent 9960befed5
commit e97f2b9c2f
8 changed files with 155 additions and 30 deletions

View File

@@ -652,11 +652,12 @@ async fn scan_one(
scan.torrents += 1;
continue;
}
match torrent_one(ctx, id, &item.url, &dest_dir).await {
match torrent_one(ctx, id, item.id, &item.url, &dest_dir).await {
Ok((path, bytes)) => {
ctx.db.mark_downloaded(&item.url, &path, bytes)?;
ctx.out.emit(Event::DownloadDone {
feed: id.to_string(),
enclosure: item.id,
url: item.url.clone(),
path: path.display().to_string(),
bytes,
@@ -667,6 +668,7 @@ async fn scan_one(
let msg = format!("{e:#}");
ctx.out.emit(Event::DownloadError {
feed: id.to_string(),
enclosure: item.id,
url: item.url.clone(),
msg: msg.clone(),
});
@@ -676,10 +678,11 @@ async fn scan_one(
}
continue;
}
match fetch_one(ctx, id, feed_cfg, &item.url, &dest_dir).await {
match fetch_one(ctx, id, item.id, feed_cfg, &item.url, &dest_dir).await {
Ok((path, bytes)) => {
ctx.out.emit(Event::DownloadDone {
feed: id.to_string(),
enclosure: item.id,
url: item.url.clone(),
path: path.display().to_string(),
bytes,
@@ -690,6 +693,7 @@ async fn scan_one(
let msg = format!("{e:#}");
ctx.out.emit(Event::DownloadError {
feed: id.to_string(),
enclosure: item.id,
url: item.url.clone(),
msg: msg.clone(),
});
@@ -727,6 +731,7 @@ fn reject(feed_cfg: &config::Feed, entry: &feed::Entry, url: &str) -> Option<&'s
async fn fetch_one(
ctx: &Ctx,
feed_id: &str,
enclosure: i64,
feed_cfg: &config::Feed,
url: &str,
dest_dir: &std::path::Path,
@@ -742,6 +747,7 @@ async fn fetch_one(
last_pct = pct;
ctx.out.emit(Event::Progress {
feed: feed_id.to_string(),
enclosure,
url: url.to_string(),
file: name.clone(),
done,
@@ -760,7 +766,7 @@ async fn fetch_one(
ctx.db.mark_enclosure(url, "skipped", Some("torrents disabled"))?;
anyhow::bail!("body is a torrent and torrents are disabled");
}
return torrent_one(ctx, feed_id, url, dest_dir).await;
return torrent_one(ctx, feed_id, enclosure, url, dest_dir).await;
}
let path = download::place(&got, dest_dir).await?;
@@ -794,10 +800,10 @@ async fn download_one(ctx: &Ctx, id: i64) -> Result<()> {
if !cfg.torrent.enabled {
Err(anyhow::anyhow!("torrents are disabled"))
} else {
torrent_one(ctx, &enc.feed_id, &enc.url, &dest_dir).await
torrent_one(ctx, &enc.feed_id, enc.id, &enc.url, &dest_dir).await
}
} else {
fetch_one(ctx, &enc.feed_id, feed_cfg, &enc.url, &dest_dir).await
fetch_one(ctx, &enc.feed_id, enc.id, feed_cfg, &enc.url, &dest_dir).await
};
match result {
@@ -805,6 +811,7 @@ async fn download_one(ctx: &Ctx, id: i64) -> Result<()> {
ctx.db.mark_downloaded(&enc.url, &path, bytes)?;
ctx.out.emit(Event::DownloadDone {
feed: enc.feed_id.clone(),
enclosure: enc.id,
url: enc.url.clone(),
path: path.display().to_string(),
bytes,
@@ -815,6 +822,7 @@ async fn download_one(ctx: &Ctx, id: i64) -> Result<()> {
ctx.db.mark_enclosure(&enc.url, "error", Some(&msg))?;
ctx.out.emit(Event::DownloadError {
feed: enc.feed_id.clone(),
enclosure: enc.id,
url: enc.url.clone(),
msg,
});
@@ -830,6 +838,7 @@ async fn download_one(ctx: &Ctx, id: i64) -> Result<()> {
async fn torrent_one(
ctx: &Ctx,
feed_id: &str,
enclosure: i64,
url: &str,
dest_dir: &std::path::Path,
) -> Result<(PathBuf, u64)> {
@@ -845,6 +854,7 @@ async fn torrent_one(
last_pct = pct;
ctx.out.emit(Event::Progress {
feed: feed_id.to_string(),
enclosure,
url: url.to_string(),
file: name.clone(),
done,