Add a feed through one function, from the CLI and the page alike (#161)

`ipx add` and the page's Add feed were the same steps written twice: find the feed, look for it in
the catalogue by same_feed, add_one, store. They had drifted, and #153's lock had to be put into
both by hand. add_to_catalogue() now does the shared part, under the lock, and answers the id and
whether the feed was there already; each caller still finds the feed itself, so the page can call a
bad address the caller's mistake (400), and then does its own part: the CLI lists or categorises,
the page subscribes and scans. add_one is private to it now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 01:53:40 +00:00
parent e710103ba6
commit 6ddd6444f9
3 changed files with 41 additions and 38 deletions

View File

@@ -794,38 +794,54 @@ async fn add(
category: Option<String>,
list: bool,
) -> Result<()> {
let mut cfg = ctx.edit_cfg().await;
let url = &feed::find_feed(&ctx.client, &feed::expand_input(url)).await?;
// Includes feeds derived from an OPML, or the same show could be added twice.
if let Some(existing) = subscriptions(ctx).await?.iter().find(|s| feed::same_feed(&s.cfg.url, url)) {
// Listing a feed the catalogue already has, or giving it a category, is the point of
// asking again: it changes those, and nothing else.
if let Some(f) = cfg.feeds.get_mut(&existing.id)
&& (list || category.is_some())
{
let url = feed::find_feed(&ctx.client, &feed::expand_input(url)).await?;
let (id, existed) = add_to_catalogue(ctx, &url, folder, keywords).await?;
// Listing a feed the catalogue already has, or giving it a category, is the point of asking
// again: it changes those, and nothing else. A feed an OPML lists has no entry to change.
if list || category.is_some() {
let mut cfg = ctx.edit_cfg().await;
if let Some(f) = cfg.feeds.get_mut(&id) {
f.listed |= list;
if category.is_some() {
f.category = category;
}
ctx.store_cfg(cfg).await?;
println!("{} is already in the catalogue; updated its Directory listing", existing.id);
return Ok(());
if existed {
println!("{id} is already in the catalogue; updated its Directory listing");
return Ok(());
}
}
anyhow::bail!("already subscribed as {:?}", existing.id);
}
let id = add_one(ctx, &mut cfg, url, folder, keywords).await?;
if let Some(f) = cfg.feeds.get_mut(&id) {
f.category = category;
f.listed = list;
if existed {
anyhow::bail!("already subscribed as {id:?}");
}
ctx.store_cfg(cfg).await?;
println!("added {id}{}", if list { ", listed in the Directory" } else { "" });
Ok(())
}
/// Adds the feed at `url` to the catalogue unless it is there already, a feed an OPML lists
/// included, so one show is never added twice: its id, and whether it was there. For `ipx add`
/// and the page's Add feed alike, which were the same steps written twice (#161); each finds the
/// feed first, to say its own way why an address is not one.
pub async fn add_to_catalogue(
ctx: &Ctx,
url: &str,
folder: Option<String>,
keywords: Vec<String>,
) -> Result<(String, bool)> {
// Taken before the duplicate check, so two people adding the same feed at once add it once.
let mut cfg = ctx.edit_cfg().await;
if let Some(existing) = subscriptions(ctx).await?.into_iter().find(|s| feed::same_feed(&s.cfg.url, url)) {
return Ok((existing.id, true));
}
let id = add_one(ctx, &mut cfg, url, folder, keywords).await?;
ctx.store_cfg(cfg).await?;
Ok((id, false))
}
/// Returns the new feed id. Adding checks the address is a feed first (`feed::find_feed`); this
/// still names one it cannot read from its URL rather than failing.
pub async fn add_one(
async fn add_one(
ctx: &Ctx,
cfg: &mut config::Config,
url: &str,