Give a feed with no category of its own one of Apple's (#117)

Most blogs and news sites name no <itunes:category>, so they sat under no chip in the
Directory unless an admin set one by hand. When TYPESAFE_KEY is set, a feed's first read, or
a forced refresh, asks TypeSafe's Jev to pick one of Apple's categories or subcategories from
the feed's title and up to 15 item titles, for a catalogue feed with neither its own category
nor an admin's. The answer is stored as the catalogue's category, so a feed is not asked
twice, and an admin can change it. Only on a first read or a forced refresh to keep the calls
down: feeds already subscribed are categorised by one `ipx fetch --force`.

Tried on eight subscribed feeds on 2026-10-04: XDA, Daring Fireball, Ars Technica and The
Verge as Tech News, The Old New Thing as Technology, John D. Cook as Mathematics,
BoardGameGeek as Games, Pluralistic as News Commentary (0.55). About 2,000 input tokens a
feed at $0.042 a million. The likeliest answer is kept however unsure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 22:15:44 +00:00
parent d238681ec1
commit 3e384f34f9
3 changed files with 103 additions and 0 deletions

View File

@@ -1526,6 +1526,28 @@ async fn scan_one(
parsed.image.as_deref(),
parsed.category.as_deref(),
).await?;
// Asked on a feed's first read, nothing of it stored yet, or when someone forces a refresh:
// each call costs, and a feed already here without a category can wait for a forced one.
// The answer becomes the catalogue's category, so a feed is not asked twice.
// A feed an OPML lists is not in the catalogue and has nowhere to keep one.
if parsed.category.is_none()
&& (force || stored.entries == 0)
&& !parsed.entries.is_empty()
&& ctx.cfg().feeds.get(id).is_some_and(|f| f.category.is_none())
&& let Ok(key) = std::env::var("TYPESAFE_KEY")
{
match feed::classify(&ctx.client, &key, &parsed).await {
Ok(category) => {
let mut cfg = (*ctx.cfg()).clone();
if let Some(f) = cfg.feeds.get_mut(id).filter(|f| f.category.is_none()) {
tracing::info!(feed = id, category = %category, "categorised");
f.category = Some(category);
ctx.store_cfg(cfg).await?;
}
}
Err(e) => tracing::warn!(feed = id, "could not categorise: {e:#}"),
}
}
if let Some(parent) = &feed_cfg.group {
let listed: Vec<(&str, &str)> = parsed