Apple Podcasts' top charts into the directory weekly, and its categories checked (#252)

With apple_country set, the directory fetches Apple's top 100 podcasts and the top 50 of each of
its 19 categories for that country (itunes.apple.com/{cc}/rss/toppodcasts), looks up each
show's RSS address (/lookup, 100 ids a call, three seconds apart, as Apple asks for no more than
about twenty a minute), and stores them as the snapshot apple-podcasts with no subscribers. The
refresher fetches them like any feed; the most-subscribed chart is unchanged. Apple's primary
genre goes in as the snapshot's category, used only while a feed names none.

The same weekly run reads Apple's genre tree (ws/genres?id=26) and warns when it differs from
ipx_feed's CATEGORIES; on 2026-10-15 they were the same, all 19 and their subcategories.

A syncs table keeps when it last ran: at start if a week has passed, again weekly, an hour after
a failure. keep_instances spares apple-podcasts, which no token holds, and no token may take
the name. A trace a sync, a span each answer.

CLAUDE.md: builds one cargo command at a time, the load looked at before and after, after a
build chained with a test compile took Tower past 200.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 13:21:48 +00:00
parent ca73b8136f
commit cd7a9b8530
10 changed files with 362 additions and 2 deletions

View File

@@ -18,6 +18,11 @@ pub struct Config {
/// publishes: `0.0.0.0:9188`. Empty serves none.
#[serde(default)]
pub metrics_bind: String,
/// Apple Podcasts' top charts for this country (its two letters: `ca`, `us`) come into the
/// directory each week, and Apple's categories are checked against ipx's (#252). Empty, or
/// left out, does neither.
#[serde(default)]
pub apple_country: String,
#[serde(default)]
pub tokens: Vec<TokenCfg>,
}
@@ -81,6 +86,14 @@ impl Config {
if !seen.insert(t.name.as_str()) {
bail!("two tokens are named {:?}", t.name);
}
// Apple's charts are filed under it; a server of that name would replace them.
if t.name == crate::apple::INSTANCE {
bail!("{:?} is the name Apple Podcasts' charts are kept under; give the token another", t.name);
}
}
let c = cfg.apple_country.trim();
if !c.is_empty() && !(c.len() == 2 && c.chars().all(|c| c.is_ascii_alphabetic())) {
bail!("apple_country is a country's two letters, such as \"ca\" or \"us\", not {c:?}");
}
Ok(cfg)
}