Skip to content

feat(plugins): library-scoped sync & recommendations with multi-instance support#31

Merged
AshDevFr merged 6 commits into
mainfrom
library-scope-sync-plugins
Jun 6, 2026
Merged

feat(plugins): library-scoped sync & recommendations with multi-instance support#31
AshDevFr merged 6 commits into
mainfrom
library-scope-sync-plugins

Conversation

@AshDevFr

@AshDevFr AshDevFr commented Jun 6, 2026

Copy link
Copy Markdown
Owner

Summary

Plugins can now be scoped to specific libraries for sync and recommendations, not just metadata, and the same integration can be registered multiple times to run against different libraries with different configuration. Every series and book sent to sync and recommendation plugins now carries the library it belongs to, and the recommendations view aggregates results across all enabled provider instances instead of silently using only the first one.

Motivation

Admins could already restrict a plugin to a subset of libraries, but that scope was honored only by metadata plugins; sync and recommendations ran over the user's entire collection and ignored it. Plugins also received no library context in their payloads, so they couldn't tell which library an entry came from. And although the same plugin could be installed more than once, the recommendations view only ever surfaced the first enabled provider, so per-library instances collapsed to one. Together these blocked the core use case: running one integration against Manga with one configuration and against Comics with another.

Changes

  • Sync: Push now only sends series from the plugin's allowed libraries, and pull only applies progress to in-scope series; when a single external ID matches duplicate series across multiple allowed libraries, all of them are updated. An empty scope continues to mean "all libraries."
  • Recommendations: Generation draws seeds only from the plugin's allowed libraries. GET /api/v1/user/recommendations now merges results across every enabled recommendation provider, deduped (highest score wins, reasons combined), with each item carrying its source plugin and provider. The response shape changed to { recommendations, sources[] } and refresh returns taskIds[]; dismissing an item removes it from every instance that surfaced it.
  • Plugin payloads: Every sync and recommendation entry now includes libraryId and libraryName.
  • Web UI: The recommendations page gains a merged/grouped view toggle, a source filter, and a per-card "via " badge. Installed official plugins offer an "Add another" action that pre-fills a non-colliding name so the same plugin can be registered per-library.
  • Operators / config: Seed config accepts a libraries list per plugin (by name; absent = all libraries) to scope instances at seed time.
  • Docs: Plugin documentation now covers the library-context payload fields, library scope on sync and recommendations, and running a plugin per-library across multiple instances.

Notes

  • No database schema change or migration; library scope defaults to "all libraries," so existing single-instance, all-library installs are unaffected.
  • The recommendations API response shape changed (top-level single-plugin fields replaced by sources[]); the frontend is updated in the same change.
  • Pull now updates all duplicate series sharing an external ID within scope, where previously it updated one arbitrary match.
  • Two same-source instances with overlapping library scope will both act on the shared series (last-write-wins). This is treated as a misconfiguration to be avoided by scoping instances to disjoint libraries; there is no runtime detection.

AshDevFr added 5 commits June 5, 2026 20:06
Sync and recommendation plugins previously received no information about
which library a series belongs to. Add libraryId and libraryName to every
entry so plugins can branch behaviour per library, which is the groundwork
for scoping a plugin (or multiple instances of one) to specific libraries.

- Add library_id/library_name to SyncEntry and UserLibraryEntry, serialized
  as libraryId/libraryName. Both default when absent so pulled entries
  returned by a plugin still deserialize.
- Add a library_names helper that batch-resolves library ids to names.
- Populate the fields from each series' library in the recommendation
  builder and both sync push builders.

Adds serde and builder tests.
Sync previously acted on a user's entire collection regardless of which
libraries a plugin was configured for. Honor the admin library_ids scope
(empty = all libraries) so a plugin only pushes and pulls progress for
series in its allowed libraries. This makes it possible to run the same
integration against different libraries as separate instances.

- Push: build_push_entries and the search-fallback builder drop series
  whose library is out of scope.
- Pull: add find_all_by_external_ids_and_source, which groups all series
  sharing an external ID instead of collapsing to one. Pull now resolves
  each match's library, skips out-of-scope matches, and applies progress
  to every in-scope duplicate (the same title across several allowed
  libraries all get updated).
- The sync handler loads the plugin and threads its allowed library set
  into both push and pull.

When scoped, a series whose library cannot be resolved is skipped
(fail-closed); unscoped behavior is unchanged. Adds repository and
handler tests.
…libraries

Recommendation generation built the user library from the user's entire
collection, ignoring the plugin's configured library scope. Honor the
admin library_ids (empty = all libraries) so a recommendation plugin only
draws seeds from the libraries it is scoped to.

- build_user_library takes an allowed-libraries set and drops out-of-scope
  series right after fetching, so all downstream batch queries stay scoped.
- The recommendations handler loads the plugin once, passes its library
  scope into the build, and reuses the same plugin for the exclude_ids
  source (removing a duplicate lookup). Because exclude_ids derives from
  the scoped library, exclusions stay in scope automatically.

Adds the first behavioral test for build_user_library covering scope
filtering and library stamping.
…der instances

GET /api/v1/user/recommendations previously surfaced only the first enabled
recommendation provider, so a user running several providers (for example the
same plugin scoped to different libraries) silently saw just one. Aggregate
across all enabled provider instances instead.

API (breaking shape change):
- Response is now { recommendations, sources[] }. The single-plugin top-level
  fields are replaced by a sources array carrying each instance's status and
  provenance; each recommendation gains sourcePlugin + source.
- Recommendations are merged and deduped by external ID — highest score wins,
  reasons combined, ordered by score — and enriched with local presence
  per-source before merging.
- Refresh enqueues a task per enabled instance and returns taskIds; it only
  conflicts when every instance is already refreshing.
- Dismiss removes the item from every instance cache that contains it and
  notifies only those plugins.

Frontend:
- The page consumes the sources array (combined "Powered by", any-cached and
  any-task-active state) and adds a merged/grouped view toggle plus a source
  filter; cards show a "via <plugin>" badge in the merged view.

Regenerates the OpenAPI spec and TypeScript types. Adds backend unit and
integration tests and frontend component tests.
Round out per-library plugin scoping with seeding support, an admin-UI
affordance for running the same plugin more than once, and docs.

- Seed config: SeedPluginConfig gains a `libraries: [name, ...]` field
  (absent = all libraries). Libraries are now seeded before plugins so the
  names resolve to library IDs; an unknown name is a hard error. The sample
  seed config documents the per-library pattern.
- Admin UI: installed official plugins now offer "Add another", and the
  create form auto-suffixes the name and display name so a second instance
  doesn't collide with the first — making it easy to run one instance per
  library with its own config.
- Docs: document the libraryId/libraryName payload fields and how library
  scope applies to sync and recommendations, including running a plugin
  per-library and the cross-instance recommendation merge.

Adds seed and UI tests.
@cloudflare-workers-and-pages

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

Copy link
Copy Markdown

Deploying codex with  Cloudflare Pages  Cloudflare Pages

Latest commit: 845924e
Status: ✅  Deploy successful!
Preview URL: https://252d8baa.codex-asm.pages.dev
Branch Preview URL: https://library-scope-sync-plugins.codex-asm.pages.dev

View logs

Sync and recommendation plugins became library-scoped, but the admin plugin
config modal never exposed the control: its Permissions tab keyed the whole
tab off "has a permissionable surface" (true only for metadata plugins), so
sync and recommendation plugins fell into a "nothing to configure" branch
that hid the Library Filter and wrongly claimed they aren't library-filtered.
Admins could only set the scope via seed config or the API.

- Separate library scoping from permissions with an isLibraryScopable helper
  (metadata, sync, and recommendation plugins; release-source excluded).
- Restructure the Permissions tab into three cases: nothing to configure
  (release-source), library filter only with a short note (sync and
  recommendation), and permissions + scopes + library filter (metadata).
  Extract a shared LibraryFilter component and correct the stale copy.

Verified in the running app: Configure → Permissions for AniList Sync and
AniList Recommendations now shows the Library Filter, reflecting each
instance's seeded scope. Updates the affected tests.
@AshDevFr AshDevFr merged commit 9d79204 into main Jun 6, 2026
19 checks passed
@AshDevFr AshDevFr deleted the library-scope-sync-plugins branch June 6, 2026 17:38
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