Skip to content

feat: add Tsundoku release-source plugin#34

Merged
AshDevFr merged 10 commits into
mainfrom
tsundoku-plugin
Jun 9, 2026
Merged

feat: add Tsundoku release-source plugin#34
AshDevFr merged 10 commits into
mainfrom
tsundoku-plugin

Conversation

@AshDevFr

@AshDevFr AshDevFr commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a new built-in release source that ingests Tsundoku's series feed and announces new volume and chapter coverage for the series a user tracks. Series are matched to the Codex catalog by their external provider IDs, with no fuzzy title matching, and each poll re-evaluates the full tracked set so newly tracked series are picked up automatically. Ships alongside host improvements that make system plugins register reliably and give them durable storage.

Motivation

Tsundoku aggregates per-series volume and chapter coverage across many upstream providers and exposes it as a single catalog-wide feed carrying provider IDs and merged coverage spans. Codex had no way to consume it. Compared to the per-series RSS approach of the existing MangaUpdates source, one filtered request covers the whole tracked set and the provider IDs allow deterministic, fuzzy-free matching that maps directly onto Codex's release candidate model.

Changes

  • Release tracking: New "Tsundoku" release source. On each poll it requests coverage for the user's tracked series and announces a release when a series' highest known volume or chapter advances; unchanged series are deduplicated and produce no new announcements.
  • Matching: Series are matched by external IDs using weighted voting across shared providers (a trusted disagreement can veto a weaker agreement), and genuinely ambiguous matches between different series are skipped rather than guessed.
  • Configuration: The source is configured with baseUrl (required), defaultLanguage (default en), pageLimit (default 100), and requestTimeoutMs (default 10000). The feed is public, so no credentials are required.
  • Plugin reliability: Newly installed release sources now appear under Settings → Release tracking immediately, instead of only after unrelated activity.
  • Operators / migration: A new database table is added to back durable, system-scoped storage for system plugins. No action is required beyond running migrations.

Notes

  • Language is a static default (en) since the feed carries none; per-series language gating still applies host-side.
  • Newly tracked series are only announced for coverage that changes after they are tracked; full historical backfill is out of scope. Pure gap-fills that do not move the highest known volume or chapter are not re-announced.
  • The source can be removed at any time from Settings → Release tracking, which prunes its ledger rows. There are no migrations to reverse for removal.

AshDevFr added 9 commits June 8, 2026 15:28
Add release-tsundoku, an api-feed release source that will consume a
Tsundoku instance's incremental series feed and announce new
volume/chapter coverage for tracked series, matched by exact external
IDs (no fuzzy matching).

This change lays the foundation: package scaffolding, the manifest
(api-feed capability declaring all supported Tsundoku external-ID
providers, plus a config schema with a required baseUrl and optional
defaultLanguage/pageLimit/requestTimeoutMs), config-driven
initialization that captures the host RPC and KV-store clients, and
single-source registration with a METHOD_NOT_FOUND retry. The poll
handler is a no-candidate stub for now; the feed walk, cursor
persistence, and matching land in follow-up changes. Tests cover base
URL normalization and source registration.

Wire the plugin into the build and CI matrices, docker-compose (backend
and worker dist mounts, dev-watch volume, build and dev commands), and
the official plugin gallery. No Makefile change is needed since it
auto-discovers plugins via a glob.
Add the building blocks for walking the Tsundoku series feed: wire types
for the feed response, a feedUrl builder, and fetchFeedPage, which wraps
fetch with a hard timeout and JSON parsing and returns a discriminated
ok/error result (passing upstream status through for host-side backoff,
and rejecting malformed bodies).

Add loadCursor/saveCursor over the plugin KV store so the feed position
survives between polls. Both are tolerant of a missing key and storage
errors: a failed read restarts the walk from the beginning and a failed
write is logged without aborting the poll, which is safe given keyset
pagination plus host-side dedup. Includes tests for the fetcher and
cursor helpers.
Add the matcher that resolves Tsundoku feed items to tracked Codex
series by exact external ID. buildIndex turns the host's tracked-series
rows into a "provider:id" -> series reverse index, and matchItem sweeps
an item's provider IDs in priority order (mangabaka first) for a stable,
fuzzy-free match.

Add the candidate mapping that turns a matched feed item into a release
candidate: coverage spans pass through onto the volume/chapter axes, the
external release id is keyed on the coverage high-water mark so a new row
fires only when the frontier advances, and confidence is fixed at 1.0
since the match is exact. Includes tests for both modules.
Wire the Tsundoku feed walk end to end. Each poll builds an exact-match
index from the host's tracked series, loads the saved feed cursor, then
walks the feed page by page: matching items by external ID, recording
hits as release candidates, and persisting the cursor after every page so
an interrupted walk resumes cleanly. Per-candidate record failures are
logged and skipped, the walk stops on a fetch error with the cursor
preserved, and the poll returns parsed/matched/recorded/deduped counters
plus the worst upstream status for host backoff.

Add a README covering setup, configuration, the exact-match model, cursor
behavior, and known limitations (default language, incremental backfill
gap, high-water dedup). Includes a poll test suite covering multi-page
walks, cursor persistence, host dedup, no-match skips, record-error
tolerance, and fetch-error handling.
…n cursor storage

Two related fixes for release-source plugins, which run as system plugins
with no per-user context.

Host: add a readiness barrier to reverse-RPC dispatch. A plugin can call a
reverse method (e.g. releases/register_sources) from onInitialize the moment
it returns its manifest — before the host finishes installing the
post-initialize context — and the dispatcher bounced it with
METHOD_NOT_FOUND, leaving registration to flaky plugin-side retries. The
context now carries a readiness notification that set_capabilities fires;
an early reverse-RPC parks until capabilities are installed (bounded by a
timeout) and then dispatches normally, falling back to the previous
METHOD_NOT_FOUND only if initialization never completes. This fixes source
registration for every release-source plugin, not just one.

Plugin (tsundoku): persist the feed cursor in the source's etag slot via
releases/source_state instead of the plugin KV store. The KV store is scoped
per user, but a release source has no user context, so storage/* is rejected
with "Storage is not available". The host hands the saved cursor back as the
poll request's etag and persists the etag returned on the poll response, so
the per-source state slot is the correct single-row bookmark. Also surface a
hard error when the first feed page can't be fetched so the source reports
last_error rather than a silent empty poll.

Includes host tests for the readiness barrier (early call succeeds once
capabilities land) and updates the plugin tests for the new cursor source.
…sundoku cursor

The plugin KV store (storage/*) was keyed by user_plugin_id, so system
plugins — which have no user context — were rejected with "Storage is not
available". Add a parallel per-plugin store so system plugins (e.g. release
sources) get durable key-value storage.

- DB: new plugin_data table keyed by plugin_id (FK to plugins, cascade,
  unique (plugin_id, key), TTL + indexes), a matching entity, and
  PluginDataRepository mirroring the per-user repository.
- Services: StorageRequestHandler is now scope-aware (user vs system) behind
  a normalized entry type, with a system constructor; the system spawn path
  installs a system-scoped storage handler keyed by the plugin row. The
  per-user storage path is unchanged.
- Tasks: the expired-data cleanup now sweeps both the user and system tables.

Migrate the Tsundoku release plugin's feed cursor from the source-state etag
slot to this KV store (feed_cursor), removing the bespoke source_state
round-trips. The source-state etag slot remains for its intended use (HTTP
ETag conditional GET); only the cursor moved. Also drop the registerSources
retry loop, now redundant thanks to the host's reverse-RPC readiness barrier.

Includes repository, handler, and plugin tests.
In distributed mode the worker records the entity events a task emits and
the web TaskListener replays them to SSE subscribers — but only for tasks
that completed. Failed tasks dropped their events, so a release poll that
errored never delivered its `release_source_polled` event and the Release
tracking UI didn't refresh until a manual reload.

Carry recorded events through the failure path: `mark_failed` now accepts
and persists result data, `fail_task` passes the recorded events into it,
and the TaskListener replays recorded events on the Failed terminal state
as well as Completed. Adds repository tests for the new result-data
persistence.
Matching on the first shared provider id was unsafe: non-MangaBaka
providers (MAL, MangaUpdates, …) sometimes share or merge ids across
distinct series, so a single matching low-trust id could attribute one
series' coverage to another.

Match by weighted voting instead. For each provider both the feed item
and a candidate series carry, an agreeing id adds its weight (MangaBaka 3,
AniList 2, rest 1) and a disagreeing one subtracts it; a series matches
only on a net-positive score, so a trusted disagreement (different
MangaBaka ids) overrides a sloppy agreement (a shared MAL id). Confidence
is derived from the score, and a post-walk cross-item pass keeps at most
one feed entry per Codex series — best score wins, and genuinely
ambiguous ties between different series are skipped rather than
mis-attributed. Candidate confidence/reason now reflect the vote instead
of a hardcoded 1.0. Tests cover the voting, conflict rejection, ambiguity,
and cross-item resolution.
Replace the catalog-wide GET + persisted cursor with Tsundoku's filtered
POST /series/feed, posting the tracked series' provider:externalId set so
the response contains only those series. There's no persisted cursor: each
poll re-walks the tracked set's current coverage and relies on host-side
dedup to suppress unchanged releases. This makes newly tracked series
backfill automatically and untracked ones drop out, with no cursor
bookkeeping — closing the new-series backfill gap by construction. When the
tracked set is empty the poll skips entirely (an empty filter would mean
"whole catalog" upstream). The system KV store and its cursor helpers are
no longer needed and are removed from the plugin.

Also fix a provider-name mismatch: Codex stores some sources under
different names than the feed uses (e.g. myanimelist vs mal, animeplanet
vs anime_planet, animenewsnetwork vs anime_news_network). Add a Codex →
Tsundoku name map; requiresExternalIds now declares the Codex names the
host filters on, and the matcher translates to the feed's names so the
filter and the vote line up across all eight providers. Tests and the
README are updated for the POST flow and the name mapping.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 9, 2026

Copy link
Copy Markdown

Deploying codex with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5cb9152
Status: ✅  Deploy successful!
Preview URL: https://a8a49cb9.codex-asm.pages.dev
Branch Preview URL: https://tsundoku-plugin.codex-asm.pages.dev

View logs

The `mark_failed` signature gained a fourth `result_data: Option<JsonValue>`
parameter when failed-task event replay was added, but the task queue tests
were not updated and failed to compile. Pass `None` at each call site since
these tests don't exercise event replay.
@AshDevFr AshDevFr merged commit dd53306 into main Jun 9, 2026
22 checks passed
@AshDevFr AshDevFr deleted the tsundoku-plugin branch June 9, 2026 03:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant