Skip to content

feature: grimmory connector - #136

Open
benjitobz wants to merge 19 commits into
Chaptarr:developfrom
benjitobz:feature/grimmory-connector
Open

feature: grimmory connector#136
benjitobz wants to merge 19 commits into
Chaptarr:developfrom
benjitobz:feature/grimmory-connector

Conversation

@benjitobz

Copy link
Copy Markdown

Description

Adds a Grimmory connection: a notification provider that refreshes Grimmory's libraries after
Chaptarr imports, renames, retags or deletes files, optionally pushes Chaptarr's metadata and
covers into Grimmory, and optionally forwards edits made in Grimmory back out to other
connections.

Library refreshes are queued at event time and drained in ProcessQueue, per media type, so a
burst of imports collapses into one refresh per library. Pushes are opt-in per connection
(Push Metadata / Push Covers) and lock the fields they write in Grimmory so its own metadata
refreshes don't overwrite them; a manual push dialog on Book Details and the book editor lets you
choose which fields to send.

Edit forwarding watches the root folders for Grimmory's sidecar writes (.metadata.json /
.cover.jpg) rather than polling, since Grimmory's database is remote. Chaptarr's own pushes also
rewrite the sidecar, so a small registry absorbs that echo and forwards only edits a person made.
Targets are discovered through IExternalLibraryEditTarget, so no concrete provider is referenced
and other connections can opt in independently.

Also includes a scan fix: an empty scan result for a subfolder of a root that provably still has
content is treated as a real delete rather than a dropped mount, so a book pruned from disk by an
external library app stops being tracked forever.

Fixes

Not a fix but resolves: #99

External Related PRs

grimmory-tools/grimmory#2567

This PR is not a blocker for this just the cover art won't get backed into the file by Grimmory. It's a small change. Confident it will get merged.

Database Migration

NO. Connection settings serialize into the existing Notifications.Settings JSON column. The new
OnLibraryFileAdded / SupportsOnLibraryFileAdded members are interface-level only.

How was this tested?

Docker on Ubuntu against Grimmory, plus a Calibre content server and AudioBookShelf
configured as sibling connections to receive forwarded edits. Edit forwarding requires Grimmory's
sidecar "write on update" setting to be on. Also see the External Related PRs

Verified:

Note: Delays between the systems are to be expected. Grimmory's library refresh is asynchronous,
and the edit forwarder debounces for 90s before sending, so several of these scenarios need a wait
rather than an immediate check.

  1. Per-media-type library refresh
    Import an ebook, then an audiobook, with both libraries selected on the connection.
    Expected: each import refreshes only its own Grimmory library. Nothing is sent at event time —
    refreshes queue and drain on ProcessQueue, so a burst of imports collapses into one refresh per
    library rather than one per file.

  2. Book added in Grimmory is adopted by the scan
    Add a book through Grimmory so the file lands in the shared root folder, then run a Chaptarr scan.
    Expected: the file is adopted and mapped to the right book/edition if it matches one, otherwise it
    shows on the unmapped files page for manual matching. Grimmory already holds the file, so no
    refresh is needed for it to appear there.

  3. Book deleted in Grimmory goes back to missing and redownloads
    Delete a book in Grimmory so its file leaves the shared storage, then run a Chaptarr scan.
    Expected: the file row is removed as MissingFromDisk and the book stays monitored — it drops back
    to missing and is eligible for search again, rather than being unmonitored. An empty scan result
    for a subfolder of a root that provably still has content is a real delete, not a dropped mount,
    so only a root-level empty scan keeps the mount guard. Before this, the row lingered forever,
    Chaptarr still believed it had the file, and the book was never re-searched.

  4. Rename, retag, and delete trigger refresh
    Rename an author's files, retag a book, delete a book with files, delete an author with files.
    Expected: each queues a refresh for the right library. Deletes now drain the queue —
    MediaFileDeletionService publishes DeleteCompletedEvent on book deletes, which it previously
    only did for author deletes, so a book delete no longer left the refresh sitting in the queue.

  5. Field selection dialog
    Open Grimmory Push on a book page.
    Expected: ten selectable fields — Cover, Title, Author, Series, Description, Publisher, Published
    Date, Language, Tags, Identifiers — each with the value that will be sent shown beside it. All are
    ticked by default. Push is disabled if you untick everything. The /books bulk dialog is identical
    minus the previews.

  6. Push writes only what it has
    Push all fields on a book with no description, publisher or genres in Chaptarr.
    Expected: empty fields are skipped, not sent — whatever Grimmory already holds survives. Only the
    fields with values appear in the request.

  7. Pushed fields come back locked
    Push title and description, then look at the book in Grimmory.
    Expected: both land and both are locked there, so Grimmory's own metadata refreshes won't
    overwrite them.

  8. Re-push respects an existing lock
    Change the description via an edition change in Chaptarr and pushing again without unlocking anything in Grimmory.
    Expected: the field does not change — Grimmory skips locked fields even for the writer that locked
    them. Unlock it in Grimmory, push again, and the new value lands. Locks stay authoritative.

  9. Auto-push waits for Grimmory's scan
    Enable Push Metadata and Push Covers, then import a new book.
    Expected: the push is queued behind the refresh and retries until the book appears in Grimmory —
    up to 90s, re-fetching the library list every 10s rather than trusting a cached one. Without the
    wait the push lands before Grimmory has ingested the file and finds nothing.

  10. Cover change in Chaptarr auto-pushes
    Change a book's cover in Chaptarr with Push Covers on.
    Expected: the new cover reaches Grimmory. Author-level cover events are deliberately ignored —
    they fire during routine author refreshes and would otherwise fan out into a push for every book
    of that author. Repeat pushes for the same book are suppressed for 5 minutes.

  11. Bulk push over a selection
    Select several books on /books, push.
    Expected: one command carries all ids; each book pushes independently, and a failure on one does
    not abort the rest.

  12. Edit made in Grimmory is forwarded out
    With Forward Grimmory Edits on, change a description or cover in Grimmory.
    Expected: after the debounce, the edit reaches the other connections that accept library edits —
    the Calibre content server and AudioBookShelf here. Identity fields stay Chaptarr's; only the
    descriptive fields mirror. Nothing is written to Chaptarr's own database.

  13. Chaptarr's own push is not echoed back
    Push metadata to Grimmory with forwarding on.
    Expected: Grimmory rewrites the sidecar in response to the push, and that write is recognised as
    Chaptarr's own and dropped.

  14. A real edit immediately after a push still forwards
    Push to Grimmory, then edit the same book in Grimmory before the debounce window closes.
    Expected: the push's echo is consumed once and the person's edit still comes out of the same
    batch. This is the case that a per-batch decision would have collapsed — echo and genuine edit
    coalescing and being discarded together.

  15. Cover-only edit in Grimmory
    Replace only the cover in Grimmory, touch no other field.
    Expected: the cover reaches the sibling connections as image bytes, not as a URL, so a target that
    cannot authenticate against Grimmory still gets it.

  16. Sibling Grimmory connection receives the edit
    Configure two Grimmory connections, edit a book in the first.
    Expected: the second one receives the forwarded edit and converges; the source connection does not
    receive its own edit back.

Screenshots (UI changes only)

Quick Start config option:
image

Connection config option:
image

Books ulk push 'Push Chaptarr metadata to Grimmory' button:
image

Books bulk push 'Push Chaptarr metadata to Grimmory' dialog after clicking:
image

Book details 'Grimmory Push' button:
image

Book details 'Push Chaptarr metadata to Grimmory' dialog with preview after clicking:
image


A note on AI: We know AI/agentic coding is everywhere and only getting
more popular. We won't insist that you disclose whether you used it or which
models you used, but in the same spirit, please don't take offense if your PR
is scrutinized and changes are requested.

Review time: The longer the PR and the more lines changed, the longer the
review will take. Small, focused PRs merge fastest. If yours is big, please be
patient.

Queues affected Grimmory libraries on import, rename, delete and retag
events and drains them via ProcessQueue after files land on disk,
replacing the archived branch's in-handler sleep and all-libraries sync
task. Book deletes now publish DeleteCompletedEvent after disk cleanup
so queued notification work (Grimmory, Plex) drains at the right time.
Two connector toggles push Chaptarr's metadata (locking pushed fields)
and cover to the matched Grimmory book on import, retag, and cover
updates. A Grimmory Push toolbar button on book details and a bulk
button in the book editor open a field-picker dialog driving the
PushGrimmoryMetadata command; both disable without files on disk.
A poller forwards edits made in Grimmory (audit log for metadata,
cover stamps for covers, own-user echoes excluded) to any connection
implementing the new IExternalLibraryEditTarget seam, keeping this
branch mergeable without the ABS/CCS branches.
Grimmory rewrites <book>.metadata.json (and .cover.jpg) next to the
book on every edit when sidecar write-on-update is enabled, so the
forwarder now watches the root folders for those writes - the same
mechanism the calibre forwarder uses on metadata.db - replacing the
2-minute audit-log poll and its admin requirement. Chaptarr's own
pushes are filtered through a recent-push registry rather than by
username, so edits made in Grimmory under the connection's account
are forwarded too.
A push makes Grimmory rewrite the sidecar exactly once, so the
suppression entry is spent on the first matching sidecar event; a
person's edit made minutes later - previously discarded for the whole
10-minute window - forwards again.
Cover-only pushes leave no sidecar echo to consume, so recording them
could swallow the next real edit made in Grimmory.
A push's echo and a person's edit made inside the same debounce window
coalesced into one batch entry and were discarded together. Each
filesystem event is now checked on arrival: the first event after a
push consumes the entry, duplicates inside a short shadow are
absorbed, and anything later is queued and forwarded.
The mount guard skipped cleanup whenever a scan found no media files,
so a granular scan of a book folder whose only file was deleted
externally (e.g. removed in Grimmory) never pruned the tracked rows
and the deletion never reached connections. An empty subfolder scan
now cleans up when the root folder itself provably has content;
root-level empty scans keep the guard.
Grimmory never updates a locked field, even for the writer who locked
it, and applies a request's values before its lock flags - so the
first pushed value froze forever and corrected re-pushes silently
no-oped. Each push now clears its target locks via toggle-field-locks
first; the update itself re-locks them. Publish date also prefers the
monitored edition's release date over the book's.
Reverts the unlock-before-write pass: a locked field in Grimmory now
stays exactly as locked, including against Chaptarr's own re-pushes.
Updating a locked value means unlocking it in Grimmory first. The
monitored-edition release date preference stays.
Restores the archived quickstart section; the card drives the standard
notification modal off the schema, so it picks up the current library
dropdowns and push/forward toggles as-is.
Grimmory now implements IExternalLibraryEditTarget, so an edit made
in one instance reaches every other configured instance alongside the
ABS and content server targets; the forwarder already excludes the
source. Applies descriptive fields and covers per each connection's
push toggles, resolves the book by root-relative path in that
connection's own libraries, respects its locks, and records the push
so the target's own sidecar rewrite is absorbed instead of ping-
ponging - Grimmory's no-change detection ends the chain once values
converge.
Grimmory writes its sidecar while the update request is still in flight, so
the filesystem event could reach the forwarder before the registry entry
existed and get forwarded back out as a real edit.
Strip the reviewer-facing narration from the Grimmory connector, keeping
only the comments that record an external constraint: Grimmory rewriting
the sidecar during a metadata update, its async refresh, its handling of
locked fields, and the AudioBookShelf rescan the forwarder debounces past.

Drop IGrimmoryProxy.GetLibraryBooks, which no caller outside the proxy
used, and inline the single-use edition lookup in GrimmoryPushService.

File the two Grimmory push strings in their alphabetical place in en.json
instead of the middle of the GoTo* run, and translate the push dialog's
field labels the way QuickstartMatchingSection does rather than hardcoding
English. Publish Date reuses the existing PublishedDate key.
Grimmory rewrites its sidecar in response to a push and the forwarder drops
that event as an echo, so Audiobookshelf and the other connections never saw
covers or metadata that were pushed from Chaptarr.
Those three were written without their lock flag, so Grimmory's own metadata
refresh could overwrite them. The cover is uploaded before the lock is set
because Grimmory rejects an upload outright once the cover is locked, and an
already-locked cover is left alone for the same reason.
A file adopted by a disk scan raises BookImportedEvent with NewDownload
false, which NotificationService dropped before any provider saw it. A book
added through Grimmory and picked up by a scan therefore got no library
refresh and no metadata or cover push until something else touched it.

Add the NotifyOnLibraryImports opt-in to INotification and NotificationBase
and let NotificationService deliver library imports to the providers that
declare it, along with an OnLibraryFileAdded hook for providers that work
per file. Grimmory declares it whenever it is already configured to push,
so a scan pickup goes through the existing OnReleaseImport path: refresh
the matching library, then push the selected fields once Grimmory's own
scan has the book.

The three shared notification files are byte-identical to the ones on
feature/calibre-content-server-connector so the two branches still merge
without conflict.
@benjitobz benjitobz changed the title Feature/grimmory connector feature: grimmory connector Sep 10, 2026
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.

[FEATURE] Add Grimmory Connect integration for library synchronization

1 participant