A spinner on the feed being checked, not toasts; check only your own feeds
The scan's events reach everyone, so every browser showed "<feed>: N new" and "Scanning…" toasts, and refreshed, for everyone's feeds. Now a feed's row, and its folder's, carries a spinner between feed_start and its done, skip or error; the list refreshes only for the reader's own feeds; the scan toasts are gone, and "Downloaded" is said only for a file on screen. "Check every feed" from the web UI sent a scan of every feed on the server. Command::Fetch takes an optional `feeds` list -- those feeds and the feeds inside any OPML among them -- and the web fills it with the asker's subscriptions. The schedule and the CLI send none, meaning every feed. Closes #37. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -88,6 +88,11 @@ pub enum Command {
|
||||
feed: Option<String>,
|
||||
#[serde(default)]
|
||||
force: bool,
|
||||
/// Only these feeds, and the feeds inside any of them that is an OPML: "check every feed"
|
||||
/// from the web UI is every feed of the person asking, not of everyone (issue #37).
|
||||
/// Empty is every feed, as the schedule and the CLI mean it.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
feeds: Vec<String>,
|
||||
},
|
||||
Reap {
|
||||
#[serde(default)]
|
||||
@@ -301,10 +306,10 @@ mod tests {
|
||||
#[test]
|
||||
fn commands_parse_from_the_wire_form() {
|
||||
let got: Command = serde_json::from_str(r#"{"cmd":"fetch"}"#).unwrap();
|
||||
assert!(matches!(got, Command::Fetch { feed: None, force: false }));
|
||||
assert!(matches!(got, Command::Fetch { feed: None, force: false, .. }));
|
||||
|
||||
let got: Command = serde_json::from_str(r#"{"cmd":"fetch","feed":"atp","force":true}"#).unwrap();
|
||||
assert!(matches!(got, Command::Fetch { feed: Some(f), force: true } if f == "atp"));
|
||||
assert!(matches!(got, Command::Fetch { feed: Some(f), force: true, .. } if f == "atp"));
|
||||
|
||||
let got: Command = serde_json::from_str(r#"{"cmd":"reap","dry_run":true}"#).unwrap();
|
||||
assert!(matches!(got, Command::Reap { dry_run: true }));
|
||||
|
||||
16
src/main.rs
16
src/main.rs
@@ -197,7 +197,7 @@ async fn main() -> Result<()> {
|
||||
// A daemon owns the state; don't have two processes downloading the same thing.
|
||||
let wire_cmd = match &cli.command {
|
||||
Command::Fetch { feed, force } => {
|
||||
Some(Cmd::Fetch { feed: feed.clone(), force: *force })
|
||||
Some(Cmd::Fetch { feed: feed.clone(), force: *force, feeds: vec![] })
|
||||
}
|
||||
Command::Reap { dry_run } => Some(Cmd::Reap { dry_run: *dry_run }),
|
||||
Command::Status => Some(Cmd::Status),
|
||||
@@ -354,10 +354,10 @@ async fn user_cmd(ctx: &Arc<Ctx>, cmd: UserCmd) -> Result<()> {
|
||||
|
||||
async fn run(ctx: &Arc<Ctx>, cmd: Cmd) -> Result<()> {
|
||||
match cmd {
|
||||
Cmd::Fetch { feed, force } => {
|
||||
Cmd::Fetch { feed, force, feeds } => {
|
||||
// Make room before pulling more down, as the original did per download.
|
||||
reap(ctx, false, false).await?;
|
||||
fetch(ctx, feed.as_deref(), force).await
|
||||
fetch(ctx, feed.as_deref(), force, &feeds).await
|
||||
}
|
||||
Cmd::Reap { dry_run } => reap(ctx, dry_run, true).await,
|
||||
Cmd::Download { enclosure } => download_one(ctx, enclosure).await,
|
||||
@@ -510,7 +510,7 @@ async fn daemon(
|
||||
}
|
||||
_ = ticker.tick() => {
|
||||
// Per-feed schedule and TTL decide what actually gets polled.
|
||||
let job = run(&ctx, Cmd::Fetch { feed: None, force: false });
|
||||
let job = run(&ctx, Cmd::Fetch { feed: None, force: false, feeds: vec![] });
|
||||
if !until_stopped(&ctx, &rx_stop, job).await {
|
||||
break;
|
||||
}
|
||||
@@ -892,7 +892,8 @@ async fn reap(ctx: &Ctx, dry_run: bool, standalone: bool) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch(ctx: &Arc<Ctx>, only: Option<&str>, force: bool) -> Result<()> {
|
||||
/// `scope`, when not empty, narrows the scan to those feeds and the feeds inside any of them.
|
||||
async fn fetch(ctx: &Arc<Ctx>, only: Option<&str>, force: bool, scope: &[String]) -> Result<()> {
|
||||
let cfg = ctx.cfg();
|
||||
let subs = subscriptions(ctx).await?;
|
||||
if let Some(id) = only
|
||||
@@ -903,7 +904,10 @@ async fn fetch(ctx: &Arc<Ctx>, only: Option<&str>, force: bool) -> Result<()> {
|
||||
|
||||
let mut scanned = 0;
|
||||
let mut fresh: Vec<String> = vec![];
|
||||
for sub in subs.iter().filter(|s| only.is_none_or(|o| o == s.id)) {
|
||||
let in_scope = |s: &Sub| {
|
||||
scope.is_empty() || scope.contains(&s.id) || s.cfg.group.as_ref().is_some_and(|g| scope.contains(g))
|
||||
};
|
||||
for sub in subs.iter().filter(|s| only.is_none_or(|o| o == s.id) && in_scope(s)) {
|
||||
let (id, feed_cfg) = (&sub.id, &sub.cfg);
|
||||
let state = ctx.db.http_state(id).await?;
|
||||
|
||||
|
||||
14
src/web.rs
14
src/web.rs
@@ -1239,7 +1239,7 @@ async fn add_feed(
|
||||
/// succeeded either way, so a daemon not taking commands is only logged.
|
||||
async fn scan_soon(state: &WebState, feed: Option<String>) {
|
||||
let force = feed.is_some();
|
||||
if state.cmds.send(Command::Fetch { feed, force }).await.is_err() {
|
||||
if state.cmds.send(Command::Fetch { feed, force, feeds: vec![] }).await.is_err() {
|
||||
tracing::warn!("could not queue a scan: the daemon is not accepting commands");
|
||||
}
|
||||
}
|
||||
@@ -1527,11 +1527,21 @@ struct FetchBody {
|
||||
|
||||
async fn fetch_now(
|
||||
State(state): State<WebState>,
|
||||
user: crate::db::User,
|
||||
Json(body): Json<FetchBody>,
|
||||
) -> Result<StatusCode, ApiError> {
|
||||
// "Check every feed" is every feed this person reads, not everyone's (issue #37).
|
||||
let feeds = if body.feed.is_some() {
|
||||
vec![]
|
||||
} else {
|
||||
state.ctx.db.subscriptions_for(user.id).await?.into_iter().map(|s| s.feed_id).collect()
|
||||
};
|
||||
if body.feed.is_none() && feeds.is_empty() {
|
||||
return Ok(StatusCode::ACCEPTED); // nothing of theirs to check; an empty list would mean all
|
||||
}
|
||||
state
|
||||
.cmds
|
||||
.send(Command::Fetch { feed: body.feed, force: body.force })
|
||||
.send(Command::Fetch { feed: body.feed, force: body.force, feeds })
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("the daemon is not accepting commands"))?;
|
||||
Ok(StatusCode::ACCEPTED)
|
||||
|
||||
Reference in New Issue
Block a user