Podping listener design (2.77.0)
Date: 2026-07-23 Status: implemented in 2.77.0
MinusPod optionally subscribes to Podping (the Podcasting 2.0 publish notification bus on the Hive blockchain) so that feeds whose hosts send podpings get picked up within seconds of publishing instead of waiting for the next 15-minute RSS poll. Polling remains the completeness guarantee; podping is an accelerator only.
Decisions made during brainstorming (user-approved):
- Accelerator only. The 15-minute polling loop is unchanged and remains the fallback for hosts that do not ping and for any missed blocks.
- In-process listener thread using raw JSON-RPC over
requestsagainst public Hive API nodes. No new pip dependencies, no sidecar container. - Opt-in: new
podping_enabledsetting, default off. - Empirical context: only some hosts are observed pinging. The feature is cheap,
self-contained, and aligned with the Podcasting 2.0 spec MinusPod
already documents; adoption grows over time and per-feed
last_podping_atgives ground truth.
Component: src/podping_listener.py
Section titled “Component: src/podping_listener.py”A leader-only daemon thread started in main_app/__init__.py next to
the RSS refresh and queue processor threads.
Loop (every iteration):
- If
podping_enabledis false,shutdown_event.wait(30)and continue. Toggle takes effect without restart. - Poll
condenser_api.get_dynamic_global_propertieson the current node forhead_block_number. - Fetch each new block via
condenser_api.get_block. If more than 100 blocks behind (startup, node outage, long GC pause), jump tohead - 1and log at INFO; polling covers the gap. - For each transaction operation of type
custom_jsonwhere the op id ispodpingor starts withpp_:- Sender check: the op’s
required_posting_authsmust intersect the allow-list (below). Otherwise skip. - Parse the JSON payload. Collect feed IRIs from
iris(v1.x list),urls(v0.x list), orurl(v0.x single). Ignore payloads that fail to parse. reasonfiltering: act only onupdate/livereasons and on v0.x payloads with no reason field. Ignore other reasons.
- Sender check: the op’s
- Match IRIs against the local feed map (below). On a match:
- Stamp
podcasts.last_podping_at(ISO UTC). - If the feed was podping-refreshed within the last 300s, log and skip (cooldown).
- Otherwise call
refresh_single_feed(slug)and log at INFO ([slug] Podping received (reason=update), refreshing feed).
- Stamp
- Sleep so the cadence is ~3s per iteration (Hive block time).
Node handling: PODPING_NODES constant vendored from the reference
watcher’s list at implementation time (verified so far:
https://api.hive.blog, https://api.openhive.network,
https://hived.emre.sh; the implementer copies the current full list
from hive-watcher.py). On request failure or malformed
response: log WARNING, rotate to the next node, back off (5s, 15s,
60s cap). The thread must never raise out of its loop; a top-level
except Exception logs logger.exception and sleeps 60s.
Allow-list: fetched at startup and refreshed hourly via
condenser_api.get_accounts [["podping"]], taking the posting
account_auths names plus podping itself. On fetch failure keep the
previous list; if never fetched, process no ops (fail closed) and
retry next iteration.
Feed map: dict of normalized source_url to slug, rebuilt at most
every 60s from the podcasts table. Normalization (applied to both
sides): lowercase scheme and host, strip a single trailing slash.
Everything else (path case, query) is preserved verbatim.
Component: refresh_single_feed
Section titled “Component: refresh_single_feed”Small extraction from refresh_all_feeds in main_app/feeds.py: the
per-podcast body (fetch RSS, detect new episodes, enqueue
auto-processing) becomes refresh_single_feed(slug);
refresh_all_feeds iterates over it. Behavior of the 15-minute loop
is unchanged. The listener calls it directly on match.
Settings
Section titled “Settings”Two new registry entries in src/database/settings.py, exposed via
GET/PUT /api/v1/settings like every other SettingSpec:
podping_enabled(bool, default false). UI: toggle in the existing Podcasting 2.0 section (Podcasting20Section.tsx) with a short description and a note that only some hosts send podpings.rss_refresh_interval_minutes(int, default 15, range 5-1440). The refresh loop reads it at the top of each cycle, so a change applies after the current wait. UI: number row in the Processing Queue section (ProcessingQueueSection.tsx). This was not previously tunable (hardcoded 900s); it pairs with podping for users who want to stretch polling on ping-covered feeds, and stands alone for everyone else.
No other podping knobs. Cadence is protocol-fixed (3s blocks), the node list is code, the 300s cooldown and 100-block skip threshold are constants.
Database
Section titled “Database”Additive migration: podcasts.last_podping_at TEXT NULL. No data
loss, no backfill. Exposed in the feed API payload as
lastPodpingAt.
- Podcasting 2.0 settings section: the enable toggle.
- Processing queue settings section: refresh interval number row.
- Feed detail page: a muted “Last podping:
” line shown only when lastPodpingAtis set. This is the user-facing diagnostic for whether their host pings and their source URL matches.
docs/podcasting-2.0.md: new “Podping” section covering both sides: MinusPod consumes podpings (this feature) and does not emit them (MinusPod is a proxy, not a publisher; served feeds change only when upstream changes, which players discover from the upstream schedule anyway). Explain opt-in, how it works (Hive block stream, no account or keys needed, outbound HTTPS to public Hive nodes every ~3s), which classes of hosts ping, and thelast_podping_atdiagnostic.docs/configuration.md: entries for both settings.docs/glossary.md: “Podping” entry.docs/how-it-works.md: one paragraph in the feed refresh section pointing at podcasting-2.0.md.- README feature list: one bullet.
Testing
Section titled “Testing”Unit tests (mocked HTTP; no live Hive calls in CI):
- Payload parsing: v0.x
urlstring, v0.xurlslist, v1.0/1.1iris; malformed JSON ignored; reason filtering. - Op filtering: id regex (
podping,pp_*), allow-list enforcement, fail-closed when allow-list never fetched. - URL normalization and matching, including trailing slash and case.
- Cooldown: second ping within 300s stamps
last_podping_atbut does not refresh. - Catch-up skip: more than 100 blocks behind jumps to head.
- Node rotation on request failure.
refresh_single_feedextraction:refresh_all_feedsbehavior unchanged (existing tests keep passing), single-feed call refreshes exactly one feed.- Settings: registry round-trip and validation ranges for both new
settings; refresh loop honors
rss_refresh_interval_minutes.
Non-goals
Section titled “Non-goals”- No automatic polling relaxation when podping is healthy (rejected in brainstorming; the manual interval setting covers it).
- No emitting podpings for MinusPod’s own served feeds.
- No block catch-up/replay beyond the 100-block window.
- No per-feed podping enable/disable.
Version
Section titled “Version”2.77.0 (next minor after the 2.76.1 release), one PR, one build, per the batching rule.