Record an episode that turns out to be a torrent, and run it off the worker (#155)

An enclosure advertised as audio whose body is a .torrent was handed to torrent_one from inside
fetch_one, which returned without mark_downloaded, and in a scan nothing else recorded it. It
stayed pending: the page never showed it downloaded, and every scan of its feed fetched the
.torrent again and ran the torrent again, on the worker, for up to the stall limit and then the
seeding time (30 and 60 minutes in production), with no other feed checked meanwhile.

It now goes the way an advertised torrent does: in the daemon, marked downloading and handed to
spawn_torrent, which records it when it finishes; in a one-off CLI run, run there and recorded.
fetch_one answers None for the first, which a scan counts as a torrent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 01:43:04 +00:00
parent 15291298aa
commit ec55682c13
2 changed files with 21 additions and 6 deletions

View File

@@ -1761,7 +1761,8 @@ async fn scan_one(
continue;
}
match fetch_one(ctx, id, item.id, feed_cfg, &item.url, &dest_dir).await {
Ok((path, bytes)) => {
Ok(None) => scan.torrents += 1,
Ok(Some((path, bytes))) => {
ctx.out.emit(Event::DownloadDone {
feed: id.to_string(),
enclosure: item.id,
@@ -1996,6 +1997,8 @@ fn merge_policy(subs: &[db::Sub], feed_cfg: &config::Feed, global: usize) -> Pol
policy
}
/// Downloads one file and records it. None when it turned out to be a torrent, handed off to run
/// on its own, which records it when it finishes.
#[tracing::instrument(name = "download", skip_all, fields(feed = feed_id, enclosure = enclosure, url = url))]
async fn fetch_one(
ctx: &Arc<Ctx>,
@@ -2004,7 +2007,7 @@ async fn fetch_one(
feed_cfg: &config::Feed,
url: &str,
dest_dir: &std::path::Path,
) -> Result<(PathBuf, u64)> {
) -> Result<Option<(PathBuf, u64)>> {
// Throttled to whole percents, as the original's lastDLStepSize guard did.
let mut last_pct = -1i64;
let name = download::filename_for(url, None);
@@ -2035,12 +2038,22 @@ async fn fetch_one(
ctx.db.mark_enclosure(url, "skipped", Some("torrents disabled")).await?;
anyhow::bail!("body is a torrent and torrents are disabled");
}
return torrent_one(ctx, feed_id, enclosure, url, dest_dir).await;
// As one advertised as a torrent goes: off the worker where it can be, and recorded either
// way. Run here and never recorded, it stayed pending, and every scan of its feed ran it
// again, on the worker, for up to the stall limit and the seeding time (#155).
if ctx.detach_torrents {
ctx.db.mark_enclosure(url, "downloading", None).await?;
spawn_torrent(ctx, feed_id.to_owned(), enclosure, url.to_owned(), dest_dir.to_owned());
return Ok(None);
}
let (path, bytes) = torrent_one(ctx, feed_id, enclosure, url, dest_dir).await?;
ctx.db.mark_downloaded(url, &path, bytes).await?;
return Ok(Some((path, bytes)));
}
let path = download::place(&got, dest_dir).await?;
ctx.db.mark_downloaded(url, &path, got.bytes).await?;
Ok((path, got.bytes))
Ok(Some((path, got.bytes)))
}
/// Runs a torrent off the command worker, so feed scans and HTTP downloads keep moving
@@ -2114,14 +2127,15 @@ async fn download_one(ctx: &Arc<Ctx>, id: i64) -> Result<()> {
if !cfg.torrent.enabled {
Err(anyhow::anyhow!("torrents are disabled"))
} else {
torrent_one(ctx, &enc.feed_id, enc.id, &enc.url, &dest_dir).await
torrent_one(ctx, &enc.feed_id, enc.id, &enc.url, &dest_dir).await.map(Some)
}
} else {
fetch_one(ctx, &enc.feed_id, enc.id, feed_cfg, &enc.url, &dest_dir).await
};
match result {
Ok((path, bytes)) => {
Ok(None) => {} // A torrent after all, running on its own.
Ok(Some((path, bytes))) => {
ctx.db.mark_downloaded(&enc.url, &path, bytes).await?;
ctx.out.emit(Event::DownloadDone {
feed: enc.feed_id.clone(),