feat(plugins): library-scoped sync & recommendations with multi-instance support#31
Merged
Conversation
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.
Deploying codex with
|
| 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 |
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
GET /api/v1/user/recommendationsnow 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 returnstaskIds[]; dismissing an item removes it from every instance that surfaced it.libraryIdandlibraryName.librarieslist per plugin (by name; absent = all libraries) to scope instances at seed time.Notes
sources[]); the frontend is updated in the same change.