Skip to content

feat(notifications): fire lifecycle triggers across the book-acquisition pipeline - #989

Open
dny238 wants to merge 3 commits into
Listenarrs:canaryfrom
dny238:feature/book-lifecycle-notification-triggers
Open

dny238 wants to merge 3 commits into
Listenarrs:canaryfrom
dny238:feature/book-lifecycle-notification-triggers

Conversation

@dny238

@dny238 dny238 commented Sep 16, 2026

Copy link
Copy Markdown

Summary

Expands notifications from the four historical triggers to a backend-served catalog covering the whole acquisition + library lifecycle, so integrations (Discord / NTFY / Slack / custom scripts) can subscribe to any stage of a book's journey. The backend is the single source of truth; the settings UI reads the catalog instead of hardcoding trigger lists.

Trigger fire points

All gated by the global enabled-trigger set (ApplicationSettings.EnabledNotificationTriggers) via a small IBookLifecycleNotifier facade.

Trigger Where it fires
book-added / book-wanted LibraryAddService + LibraryAddWorkflow (on add; book-wanted when monitored)
book-grabbed / book-downloading DownloadService (at client submission)
book-download-completed / book-download-failed DownloadMonitorService
book-imported ImportFinalizationService (post-commit)
book-import-failed DownloadProcessingJobProcessor (terminal failure, incl. retry exhaustion)
book-available ScanJobProcessor (existing; now uses the shared constant)
book-completed MoveJobProcessor (post-move/organize)
book-renamed RenameService (per successful rename)
book-deleted LibraryDeleteWorkflow (single-item delete)

Foundation

  • NotificationTriggers catalog (id / label / description / order) — single source of truth. GET /notifications/triggers serves it.
  • IBookLifecycleNotifier resolves settings and dispatches to the configured webhook + enabled-trigger gate; best-effort (swallows non-fatal errors, re-throws cancellation).
  • Default set now derives from NotificationTriggers.DefaultEnabled — intentionally the historical four (book-added, book-downloading, book-available, book-completed) so an upgrade emits no new notification types unless the user opts in.
  • Discord content builder is data-driven, giving every trigger a friendly message instead of a raw [trigger] fallback.
  • book-upgraded is reserved (documented constant) but deliberately not catalogued: the import path has no reliable upgrade-completion signal yet (a replacement is indistinguishable from a first import), and this PR avoids advertising a trigger that never fires.

Frontend

NotificationsTab.vue fetches GET /notifications/triggers and drives the trigger list / order / labels / icons from it, with a seeded fallback so the form still renders if the request fails.

Tests

  • Catalog integrity (unique ids, ascending order, default ⊆ catalog, IsKnown, reserved-upgraded not catalogued)
  • Controller endpoint returns the full ordered catalog
  • Notifier forwards trigger/payload with the configured webhook + enabled set; swallows errors; honors cancellation
  • Per-trigger Discord content phrasing

Backend build + architecture gates green (file-focus + test-class conventions); fe type-check green.

Notes

  • No behavior change for existing installs unless they enable the new triggers.
  • New trigger ids are stable/kebab-case for forward-compatible integrations.

🤖 Generated with Claude Code

dny238 and others added 2 commits September 15, 2026 17:00
…tored add

- New Listenarr.Domain.Notifications.NotificationTriggers: central catalog of the
  full book lifecycle (acquisition + library-management) trigger names, with
  display metadata + default-enabled set (single source of truth vs. magic strings).
- GET /api/v1/notifications/triggers serves the catalog (frontend will render from it).
- Fire book-wanted (in addition to book-added) when a monitored book is added, on
  both the service and API add paths.

First increment of the comprehensive lifecycle-notification feature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ion pipeline

Expand notifications from the four historical triggers to a backend-served
catalog covering the whole acquisition + library lifecycle, so integrations
(Discord/NTFY/Slack/custom scripts) can subscribe to any stage.

New trigger fire points (all gated by the global enabled-trigger set):
- book-wanted / book-added      LibraryAddService + LibraryAddWorkflow (add + monitored)
- book-grabbed / book-downloading DownloadService (at client submission)
- book-download-completed / book-download-failed  DownloadMonitorService
- book-imported                 ImportFinalizationService (post-commit)
- book-import-failed            DownloadProcessingJobProcessor (terminal failure incl. retry exhaustion)
- book-available                ScanJobProcessor (existing; now uses the constant)
- book-completed                MoveJobProcessor (post-move/organize)
- book-renamed                  RenameService (per successful rename)
- book-deleted                  LibraryDeleteWorkflow (single-item delete)

Foundation:
- NotificationTriggers catalog (id/label/description/order) is the single source
  of truth; NotificationsController exposes GET /notifications/triggers.
- IBookLifecycleNotifier facade resolves settings and dispatches to the configured
  webhook + enabled-trigger gate (best-effort; swallows non-fatal errors).
- ApplicationSettings default now derives from NotificationTriggers.DefaultEnabled,
  intentionally the historical four so upgrades emit no new notification types
  unless the user opts in.
- Discord content builder is data-driven, giving every trigger a friendly message
  instead of a raw "[trigger]" fallback.
- book-upgraded reserved (documented) but not catalogued: no reliable
  upgrade-completion signal exists in the import path yet.

Frontend: NotificationsTab consumes GET /notifications/triggers (single source of
truth) with a seeded fallback; trigger list/order/labels/icons follow the catalog.

Tests: catalog integrity, controller endpoint, notifier dispatch/error-swallowing,
and per-trigger Discord content phrasing. Backend build + architecture gates green;
fe type-check green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dny238
dny238 requested a review from a team September 16, 2026 04:51
…ing files

Completes the reserved book-upgraded trigger. The import processor now takes a
pre-import file count for the audiobook (before ImportDownloadFilesAsync mutates
anything); a successful import into a book that already had files is a
replacement/upgrade rather than a first import. That boolean is persisted on the
job (alongside SourceRetained, so it survives the FilesImported checkpoint on a
resume) and threaded into ImportFinalizationService.FinalizeAsync, which fires
book-upgraded right after book-imported.

- NotificationTriggers: book-upgraded is now catalogued (order 55, between
  Imported and Available) so the settings UI can subscribe to it. Left out of
  DefaultEnabled (opt-in), consistent with the back-compat policy.
- No grab-time quality decision is persisted, so this is the "replaced an
  existing book's files" proxy rather than a strict quality comparison — which
  for Listenarr's automatic acquisition (it only re-grabs on an allowed upgrade)
  is an upgrade in practice. Documented as such.

Tests: FinalizeAsync fires book-upgraded when wasUpgrade is true and not on a
first import; catalog test updated to assert book-upgraded is now catalogued.
Build + arch gates green; 103 download-processing/notification tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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