Skip to content

feat(notifications): add shoutrrr and notifiarr#1371

Merged
s0up4200 merged 63 commits intodevelopfrom
feat/notifications-automation-crossseed
Feb 17, 2026
Merged

feat(notifications): add shoutrrr and notifiarr#1371
s0up4200 merged 63 commits intodevelopfrom
feat/notifications-automation-crossseed

Conversation

@s0up4200
Copy link
Copy Markdown
Collaborator

@s0up4200 s0up4200 commented Jan 30, 2026

Summary

  • Add Shoutrrr notification targets w/ expanded event coverage (torrents, backups/scans, cross-seed, automations)
  • Discord embeds + Notifiarr fields for structured messages and metadata
  • Notifications UI grouped by category with accordion + per-group select/clear
  • Align shoutrrr dep (v0.13.1), remove local replace, update docs

Summary by CodeRabbit

  • New Features

    • Full notifications system (Discord, Notifiarr, Slack, Telegram, Gotify, etc.) with test/send flows and rich payloads
    • Settings → Notifications UI to create/edit/delete targets, select/group event types, and run tests
    • Notifications emitted for torrent completion, backups (success/failure), dir/ orphan scans, cross-seed runs, and automations
    • Public HTTP endpoints to list events, manage targets, and test delivery
  • Database

    • Migration adds notifications table with updated_at trigger
  • Documentation

    • Added Notifications feature guide with setup and event descriptions

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 30, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Added a new in-process notifications system (service, types, storage, API, UI) and wired it into core services and startup so services emit rich notification events on success/failure/completion.

Changes

Cohort / File(s) Summary
Notification Service Core
internal/services/notifications/service.go, internal/services/notifications/types.go, internal/services/notifications/notifiarr_api.go, internal/services/notifications/notifiarr_api_test.go
New notifications service: event model, queue/worker dispatch, payload formatting (Discord/Notifiarr), SendTest/ValidateURL, Notifiarr integration and tests.
Notification Storage & DB
internal/models/notifications.go, internal/database/migrations/060_add_notifications.sql
New NotificationTarget model and NotificationTargetStore with full CRUD; DB migration adds table and updated_at trigger.
API Handlers & Server Wiring
internal/api/handlers/notifications.go, internal/api/server.go, internal/api/server_test.go, internal/api/handlers/backups_test.go
New HTTP handlers and routes for events/targets; Server and Dependencies extended to include NotificationTargetStore and NotificationService; tests updated.
Service Integrations
internal/services/crossseed/service.go, internal/services/automations/service.go, internal/services/orphanscan/service.go, internal/services/dirscan/service.go, internal/backups/service.go, plus related tests
Core services gain a notifications.Notifier dependency and emit Event notifications (completion/failure/summary) with instance/run context; constructors and call sites updated.
Startup / CLI Wiring
cmd/qui/main.go, go.mod
Notification components initialized/started at app startup and injected into services and API wiring; shoutrrr dependency added.
Frontend & Types
web/src/components/settings/NotificationsManager.tsx, web/src/lib/api.ts, web/src/pages/Settings.tsx, web/src/routes/_authenticated/settings.tsx, web/src/types/index.ts
Adds NotificationsManager UI, new Settings tab, API client endpoints, TypeScript types for notification entities, and routes integration.
Docs, Scripts & Misc
documentation/docs/features/notifications.md, scripts/notifiarr-test-events.go, web/src/components/instances/preferences/*
Feature documentation, Notifiarr test script, small frontend whitespace fixes and examples.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Client/UI
    participant API as HTTP API
    participant H as NotificationsHandler
    participant S as notifications.Service
    participant Q as EventQueue
    participant W as Worker
    participant B as ExternalBackend

    rect rgba(100,150,200,0.5)
        UI->>API: POST /api/notifications/targets (create)
        API->>H: CreateTarget(req)
        H->>S: store.Create(ctx, target)
        S-->>H: created target
        H-->>API: 201 Created
    end

    rect rgba(150,100,200,0.5)
        Note over UI,B: Event emission & dispatch
        S->>Q: Notify(ctx, Event)
        Q->>W: Dequeue Event
        W->>S: ListEnabledTargets(event.Type)
        loop for each target
            W->>S: Format payload for target
            W->>B: Send payload (Discord/Notifiarr/other)
            B-->>W: Response
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • Audionut

Poem

🐰 Hop, hop, a bell and a ping,

Events take flight on tiny spring,
Targets lined up, webhooks in tune,
I nibble logs beneath the moon,
Toasts and pings — impatient boon! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.42% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(notifications): add shoutrrr and notifiarr' accurately captures the main feature addition: integrating Shoutrrr and Notifiarr notification backends into the application's notification system.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/notifications-automation-crossseed

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@internal/services/notifications/service.go`:
- Around line 533-542: Replace the if-else chain that checks
strings.EqualFold(label, ...) by converting label to lowercase once (e.g., l :=
strings.ToLower(label)) and switching on l: case "torrent": description = value;
case "run": description = fmt.Sprintf("Run %s", value); default: description =
fmt.Sprintf("%s: %s", label, value); then keep the existing continue; this
change should be applied in the same function in
internal/services/notifications/service.go where the variables description and
label are used.

In `@web/src/components/settings/NotificationsManager.tsx`:
- Around line 64-72: The useEffect in NotificationsManager marks initialized too
early, causing defaults from eventDefinitions to be skipped when they load after
first render; modify the effect so setInitialized(true) is called only inside
the branches that actually populate eventTypes: call
setEventTypes(initial.eventTypes ?? []) and then setInitialized(true) when
initial exists, and in the else-if branch setEventTypes(eventDefinitions.map(e
=> e.type)) and then setInitialized(true); leave setInitialized untouched when
neither branch runs so the effect can re-run once eventDefinitions become
available.

In `@web/src/pages/Settings.tsx`:
- Around line 274-358: The new API key (newKey) is not cleared when the
create-dialog is dismissed via overlay/ESC, so add logic to clear it when the
dialog closes: update the dialog's close handler (where
setShowCreateDialog(false) is called or by adding an onOpenChange/onClose prop
on the Dialog component controlling show state) to also call setNewKey(null),
ensuring newKey is cleared on any dismissal path (overlay, ESC, or programmatic
close). Reference newKey and the setters setNewKey and setShowCreateDialog (or
the Dialog's onOpenChange handler) to implement this single-step clear when the
dialog transitions to closed.
🧹 Nitpick comments (4)
go.mod (1)

81-81: Consider moving shoutrrr to the direct dependencies block.

The shoutrrr library is placed in the indirect dependencies block (lines 50-104) but appears to be a direct dependency used by the notifications service. If it's directly imported by your code, it should be in the first require block (lines 5-46) for clarity and consistency with Go module conventions.

Alternatively, if go mod tidy placed it here, it may be transitively resolved—verify with go mod why github.com/nicholas-fedor/shoutrrr.

docs/notifications.md (1)

47-48: Format the bare URL as a markdown link.

The URL should be wrapped in angle brackets or formatted as a proper markdown link to comply with markdown best practices and fix the linting warning.

📝 Suggested fix
 See the Shoutrrr documentation for the full list of services and URL formats:
-https://github.com/nicholas-fedor/shoutrrr
+<https://github.com/nicholas-fedor/shoutrrr>
internal/services/crossseed/service.go (1)

7445-7454: Optional: avoid fmt.Sprintf for fixed prefixes in notification lines.
This aligns with the perfsprint hint and trims a little overhead in a hot-ish helper.

♻️ Suggested change
-		lines = append(lines, fmt.Sprintf("Message: %s", strings.TrimSpace(*run.Message)))
+		lines = append(lines, "Message: "+strings.TrimSpace(*run.Message))
-		lines = append(lines, fmt.Sprintf("Error: %s", strings.TrimSpace(*run.ErrorMessage)))
+		lines = append(lines, "Error: "+strings.TrimSpace(*run.ErrorMessage))
-		lines = append(lines, fmt.Sprintf("Error: %s", runErr.Error()))
+		lines = append(lines, "Error: "+runErr.Error())
-		lines = append(lines, fmt.Sprintf("Samples: %s", formatSampleText(samples)))
+		lines = append(lines, "Samples: "+formatSampleText(samples))
internal/services/automations/service.go (1)

102-123: Replace fmt.Sprintf with string concatenation per linter.

The static analysis tool correctly identifies that these simple string concatenations don't require fmt.Sprintf.

♻️ Proposed fix for perfsprint linter warnings
 func (s *automationSummary) message() string {
 	if s == nil {
 		return ""
 	}
 	lines := []string{fmt.Sprintf("Applied: %d", s.applied)}
 	if s.failed > 0 {
 		lines = append(lines, fmt.Sprintf("Failed: %d", s.failed))
 	}
 	if formatted := formatActionCounts(s.appliedByAction, 3); formatted != "" {
-		lines = append(lines, fmt.Sprintf("Top actions: %s", formatted))
+		lines = append(lines, "Top actions: "+formatted)
 	}
 	if formatted := formatActionCounts(s.failedByAction, 3); formatted != "" {
-		lines = append(lines, fmt.Sprintf("Top failures: %s", formatted))
+		lines = append(lines, "Top failures: "+formatted)
 	}
 	if len(s.sampleTorrents) > 0 {
-		lines = append(lines, fmt.Sprintf("Samples: %s", strings.Join(s.sampleTorrents, "; ")))
+		lines = append(lines, "Samples: "+strings.Join(s.sampleTorrents, "; "))
 	}
 	if len(s.sampleErrors) > 0 {
-		lines = append(lines, fmt.Sprintf("Errors: %s", strings.Join(s.sampleErrors, "; ")))
+		lines = append(lines, "Errors: "+strings.Join(s.sampleErrors, "; "))
 	}
 	return strings.Join(lines, "\n")
 }

Comment thread internal/services/notifications/service.go
Comment thread web/src/components/settings/NotificationsManager.tsx
Comment thread web/src/pages/Settings.tsx
@s0up4200 s0up4200 changed the title feat/notifications automation crossseed feat(notifications): add shoutrrr and notifiarr Jan 30, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@web/src/components/settings/NotificationsManager.tsx`:
- Around line 467-469: The CardDescription currently renders the raw target.url
in NotificationsManager which may expose tokens; update rendering to use a
sanitizer/masking helper (e.g., maskNotificationUrl or formatTargetUrl) that
detects scheme://[userinfo@]host... and replaces any userinfo/token portion with
a placeholder like **** or only returns the scheme/service name (e.g.,
"discord://••••"). Locate where target.url is used in NotificationsManager and
call this helper instead of outputting target.url directly, ensuring the helper
handles common Shoutrrr schemes and falls back to a safe masked string for
unknown formats.
🧹 Nitpick comments (8)
internal/services/notifications/service.go (5)

85-95: Workers lack graceful shutdown and WaitGroup tracking.

When ctx is cancelled, the workers exit but there's no mechanism to wait for in-flight dispatches to complete. If the service is stopped during active sends, notifications may be dropped silently.

Consider adding a sync.WaitGroup to track active workers and a Stop() method that waits for completion.

♻️ Suggested approach
 type Service struct {
 	store         *models.NotificationTargetStore
 	instanceStore *models.InstanceStore
 	logger        zerolog.Logger
 	queue         chan Event
 	startOnce     sync.Once
+	wg            sync.WaitGroup
 }
 func (s *Service) Start(ctx context.Context) {
 	if s == nil {
 		return
 	}
 
 	s.startOnce.Do(func() {
 		for range defaultWorkers {
+			s.wg.Add(1)
-			go s.worker(ctx)
+			go func() {
+				defer s.wg.Done()
+				s.worker(ctx)
+			}()
 		}
 	})
 }
+
+func (s *Service) Stop() {
+	if s == nil {
+		return
+	}
+	s.wg.Wait()
+}

122-131: Worker can block indefinitely on closed context if queue is empty.

When the context is cancelled, if the queue channel is empty, the worker correctly exits via <-ctx.Done(). However, if s.queue is closed elsewhere (e.g., during shutdown), reading from a closed channel returns the zero value immediately, potentially causing an infinite loop of dispatching empty events.

Consider checking for channel closure or using a dedicated done channel.

♻️ Safer worker loop
 func (s *Service) worker(ctx context.Context) {
 	for {
 		select {
 		case <-ctx.Done():
 			return
-		case event := <-s.queue:
+		case event, ok := <-s.queue:
+			if !ok {
+				return
+			}
 			s.dispatch(ctx, event)
 		}
 	}
 }

163-176: Context parameter is unused in send method.

The ctx parameter is ignored (named _), but it should be propagated to the underlying send methods for cancellation support, especially for HTTP-based backends like Discord and Notifiarr.

♻️ Propagate context
-func (s *Service) send(_ context.Context, target *models.NotificationTarget, event Event, title, message string) error {
+func (s *Service) send(ctx context.Context, target *models.NotificationTarget, event Event, title, message string) error {
 	if target == nil {
 		return errors.New("notification target required")
 	}
 
 	switch targetScheme(target.URL) {
 	case "discord":
-		return s.sendDiscord(target.URL, event, title, message)
+		return s.sendDiscord(ctx, target.URL, event, title, message)
 	case "notifiarr":
-		return s.sendNotifiarr(target.URL, event, title, message)
+		return s.sendNotifiarr(ctx, target.URL, event, title, message)
 	default:
-		return s.sendDefault(target.URL, title, message)
+		return s.sendDefault(ctx, target.URL, title, message)
 	}
 }

228-264: Event parameter is unused in sendNotifiarr.

The event Event parameter is received but never used, unlike in sendDiscord where it's used for color selection. Either remove it for consistency or use it for additional metadata.


376-382: Empty eventTypes slice allows all events—document this behavior.

When eventTypes is empty, allowsEvent returns true for any event type. This might be intentional (default to all events), but it's not obvious. Consider adding a comment to clarify the design decision.

📝 Add clarifying comment
+// allowsEvent returns true if the target accepts the given event type.
+// An empty eventTypes slice means "all events are allowed" (default behavior).
 func allowsEvent(eventTypes []string, eventType EventType) bool {
 	if len(eventTypes) == 0 {
 		return true
 	}
 
 	return slices.Contains(eventTypes, string(eventType))
 }
web/src/components/settings/NotificationsManager.tsx (3)

126-162: Event grouping logic is hardcoded; consider making it data-driven.

The grouping logic uses hardcoded event type strings and prefix checks. If new event types are added, this code must be updated. Consider deriving groups from the NotificationEventDefinition data (e.g., adding a category field to the API response).


471-497: Test button lacks visual feedback during pending state.

While the button is disabled during testMutation.isPending, there's no spinner or loading indicator to show that a test is in progress. Consider adding a loading state similar to other buttons.

♻️ Add loading indicator
 <Button
   variant="ghost"
   size="sm"
   onClick={() => testMutation.mutate(target.id)}
   aria-label={`Send test to ${target.name}`}
   disabled={testMutation.isPending}
 >
-  <Send className="h-4 w-4" />
+  {testMutation.isPending ? (
+    <Loader2 className="h-4 w-4 animate-spin" />
+  ) : (
+    <Send className="h-4 w-4" />
+  )}
 </Button>

388-401: Backend behavior aligns with UI—document the coupling for clarity.

When events.length === 0, the UI displays "All events" badge. The backend's allowsEvent function in internal/services/notifications/service.go returns true for empty event type slices, so the behaviors match. Consider adding a comment in either the frontend or backend code to document this intentional coupling, since it's not immediately obvious that empty means "allow all."

Comment thread web/src/components/settings/NotificationsManager.tsx
@s0up4200 s0up4200 added enhancement New feature or request notifications labels Jan 30, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@web/src/components/settings/NotificationsManager.tsx`:
- Around line 571-576: The Delete button (AlertDialogAction) currently calls
deleteMutation.mutate multiple times because it stays clickable while the
mutation is in-flight; update the AlertDialogAction to be disabled while the
mutation is pending by binding its disabled prop to the mutation loading state
(e.g., deleteMutation.isLoading or equivalent) and keep the existing onClick
handler using deleteTarget.id; optionally add an accessibility hint like
aria-busy or a spinner when deleteMutation.isLoading to indicate progress.
🧹 Nitpick comments (1)
web/src/components/settings/NotificationsManager.tsx (1)

502-510: Test button pending state applies globally across all targets.

The disabled={testMutation.isPending} check disables all test buttons when any single test is in progress. This is simple but may confuse users if they expect to test multiple targets concurrently. Consider tracking the specific target ID being tested if independent testing is desired.

💡 Optional: Track specific target being tested
+ const [testingTargetId, setTestingTargetId] = useState<number | null>(null)

  const testMutation = useMutation({
    mutationFn: (id: number) => api.testNotificationTarget(id),
    onSuccess: () => {
+     setTestingTargetId(null)
      toast.success("Test notification sent")
    },
    onError: (error: unknown) => {
+     setTestingTargetId(null)
      toast.error(getErrorMessage(error, "Failed to send test notification"))
    },
  })

  // In the button:
  <Button
    variant="ghost"
    size="sm"
-   onClick={() => testMutation.mutate(target.id)}
+   onClick={() => {
+     setTestingTargetId(target.id)
+     testMutation.mutate(target.id)
+   }}
    aria-label={`Send test to ${target.name}`}
-   disabled={testMutation.isPending}
+   disabled={testingTargetId !== null}
  >
+   {testingTargetId === target.id ? (
+     <Loader2 className="h-4 w-4 animate-spin" />
+   ) : (
      <Send className="h-4 w-4" />
+   )}
  </Button>

Comment thread web/src/components/settings/NotificationsManager.tsx
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@web/src/components/settings/NotificationsManager.tsx`:
- Around line 519-526: The trash icon Button that calls setDeleteTarget should
be disabled while a deletion is pending to prevent changing deleteTarget
mid-flight; update the Button (the one with onClick={() =>
setDeleteTarget(target)} and Trash2 icon) to use the same pending condition used
to disable the AlertDialogAction (e.g., deletionPending, isDeleting or the
deleting target id) by adding disabled={deletionPending} (and optionally
aria-disabled={deletionPending}) and guard the onClick so it no-ops when
deletionPending is true; this ensures the dialog cannot be re-opened for another
target while a deletion is in progress.
🧹 Nitpick comments (1)
web/src/components/settings/NotificationsManager.tsx (1)

418-420: Semantically misleading badge for empty event types.

When events.length === 0, displaying "All events" is misleading—an empty array means no events are subscribed, not all. While the form validation prevents saving targets with zero events, this could still cause confusion if the backend ever returns an empty array or for defensive clarity.

♻️ Suggested improvement
 const renderEventBadges = (events: string[]) => {
   if (events.length === 0) {
-    return <Badge variant="secondary">All events</Badge>
+    return <Badge variant="secondary">No events</Badge>
   }

Comment thread web/src/components/settings/NotificationsManager.tsx
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@web/src/components/settings/NotificationsManager.tsx`:
- Around line 418-431: renderEventBadges currently treats an empty events array
as "All events", which is misleading if empty means "no events"; update
renderEventBadges (and any callers that pass eventTypes) to distinguish
empty-from-all by: when events.length === 0 return a Badge variant="secondary"
with text like "No events" (or "None") and, if there is a semantic convention
that empty means all, instead add explicit handling where eventTypes is
null/undefined means "All events" and document that; ensure the change
references renderEventBadges and the eventTypes property so callers and API
consumers remain consistent.
🧹 Nitpick comments (1)
web/src/components/settings/NotificationsManager.tsx (1)

156-192: Consider extracting event categorization logic.

The hardcoded event type strings for categorization (e.g., "torrent_completed", "backup_succeeded") could become fragile if new event types are added server-side. Consider having the backend include a category field in NotificationEventDefinition to avoid client-side maintenance.

Comment thread web/src/components/settings/NotificationsManager.tsx Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
internal/services/crossseed/service.go (1)

1946-2012: ⚠️ Potential issue | 🟠 Major

Use context.WithoutCancel(ctx) for notifications to prevent skipping them on cancellation.

When the run context is canceled (user cancel, timeout, or request-scoped context), the Notify call can fail because dispatch calls store.ListEnabled(ctx), which will return an error if the context is already canceled. This causes the notification to be skipped entirely.

Use context.WithoutCancel(ctx) to ensure notifications are always sent, even when the run context is canceled. The codebase already uses this pattern for database updates in the same function (lines 157-163).

🔧 Suggested fix
-	var runErr error
-	defer func() {
-		s.notifyAutomationRun(ctx, run, runErr)
-	}()
+	var runErr error
+	notifyCtx := context.WithoutCancel(ctx)
+	defer func() {
+		s.notifyAutomationRun(notifyCtx, run, runErr)
+	}()
internal/services/dirscan/service.go (1)

584-603: ⚠️ Potential issue | 🟡 Minor

Avoid DirScanFailed notifications with unknown instance IDs.

When directory lookup fails, markRunFailed can be invoked with instanceID=0, leading to EventDirScanFailed with an invalid instance ID. Consider skipping notification when instanceID is unknown.

🛠️ Proposed guard
 func (s *Service) markRunFailed(_ context.Context, runID int64, errMsg string, instanceID int, l *zerolog.Logger) {
 	if err := s.store.UpdateRunFailed(context.Background(), runID, errMsg); err != nil {
 		l.Error().Err(err).Msg("dirscan: failed to mark run as failed")
 		return
 	}
+	if instanceID <= 0 {
+		return
+	}

 	s.notify(context.Background(), notifications.Event{
 		Type:         notifications.EventDirScanFailed,
 		InstanceID:   instanceID,
 		DirScanRunID: runID,
 		ErrorMessage: errMsg,
 	})
 }

Also applies to: 2164-2183

🤖 Fix all issues with AI agents
In `@cmd/qui/main.go`:
- Around line 599-626: The completion handler registered with
syncManager.SetTorrentCompletionHandler contains a redundant nil check for
notificationService; remove the inner "if notificationService != nil { ... }"
block and unindent its contents so the handler directly builds
trackerDomain/tags and calls notificationService.Notify. Keep the existing guard
that initializes/validates notificationService earlier, and ensure you reference
the same captured variable (notificationService) inside the closure when calling
Notify so syncManager.SetTorrentCompletionHandler's closure uses the
already-validated service.

In `@internal/services/notifications/service.go`:
- Around line 63-70: The notificationTask struct currently stores a
request-scoped context (ctx context.Context) which causes the containedctx lint
failure; remove the ctx field and change the queue to only carry Event values
(or change uses of notificationTask to enqueue Event) and update the
worker/consumer to use the service's long-lived context (e.g.,
service.run/worker loop using s.ctx or svcCtx) when dispatching; locate and
update all places that construct or read notificationTask (enqueue call sites
and the worker that dequeues and calls dispatch/handle functions) to stop
passing/expecting ctx and instead use the worker/service context for any
context-aware calls.
🧹 Nitpick comments (1)
cmd/qui/main.go (1)

577-583: Redundant nil check can be simplified.

The notificationService != nil check at line 581 appears redundant since notifications.NewService is called immediately above and doesn't return an error - it should always return a non-nil service. If NewService can legitimately return nil in some cases, consider documenting that behavior; otherwise, this check adds unnecessary cognitive overhead.

♻️ Suggested simplification
 notificationTargetStore := models.NewNotificationTargetStore(db)
 notificationService := notifications.NewService(notificationTargetStore, instanceStore, log.Logger.With().Str("module", "notifications").Logger())
 notificationCtx, notificationCancel := context.WithCancel(context.Background())
 defer notificationCancel()
-if notificationService != nil {
-    notificationService.Start(notificationCtx)
-}
+notificationService.Start(notificationCtx)

Comment thread cmd/qui/main.go
Comment thread internal/services/notifications/service.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/services/automations/service.go (1)

2091-2213: ⚠️ Potential issue | 🟡 Minor

Tag activity summary can over‑report when tag updates fail.

If any tag batch errors, the success summary still counts all planned changes. Consider suppressing success summary on failures (or tracking successful hashes) so notifications reflect reality.

🛠️ Proposed fix (suppress success summary when any tag batch fails)
-	if len(tagChanges) > 0 {
+	if len(tagChanges) > 0 {
+		tagFailures := false
 		// Try SetTags first (more efficient for qBit 5.1+)
 		// Group by desired tag set for batching
 		setTagsBatches := make(map[string][]string) // key = sorted tags, value = hashes
@@
-			if err != nil {
+			if err != nil {
 				// Check if it's an unsupported version error
 				if strings.Contains(err.Error(), "requires qBittorrent") {
 					useSetTags = false
 					break
 				}
 				log.Warn().Err(err).Int("instanceID", instanceID).Strs("tags", tags).Int("count", len(batch)).Msg("automations: set tags failed")
+				tagFailures = true
 			} else {
 				log.Debug().Int("instanceID", instanceID).Strs("tags", tags).Int("count", len(batch)).Msg("automations: set tags on torrents")
 			}
@@
-				for _, batch := range batches {
-					if err := s.syncManager.AddTorrentTags(ctx, instanceID, batch, []string{tag}); err != nil {
+				for _, batch := range batches {
+					if err := s.syncManager.AddTorrentTags(ctx, instanceID, batch, []string{tag}); err != nil {
 						log.Warn().Err(err).Int("instanceID", instanceID).Str("tag", tag).Int("count", len(batch)).Msg("automations: add tags failed")
+						tagFailures = true
 					} else {
 						log.Debug().Int("instanceID", instanceID).Str("tag", tag).Int("count", len(batch)).Msg("automations: added tag to torrents")
 					}
 				}
 			}
@@
-				for _, batch := range batches {
-					if err := s.syncManager.RemoveTorrentTags(ctx, instanceID, batch, []string{tag}); err != nil {
+				for _, batch := range batches {
+					if err := s.syncManager.RemoveTorrentTags(ctx, instanceID, batch, []string{tag}); err != nil {
 						log.Warn().Err(err).Int("instanceID", instanceID).Str("tag", tag).Int("count", len(batch)).Msg("automations: remove tags failed")
+						tagFailures = true
 					} else {
 						log.Debug().Int("instanceID", instanceID).Str("tag", tag).Int("count", len(batch)).Msg("automations: removed tag from torrents")
 					}
 				}
 			}
@@
-		if len(addCounts) > 0 || len(removeCounts) > 0 {
+		if !tagFailures && (len(addCounts) > 0 || len(removeCounts) > 0) {
 			detailsJSON, _ := json.Marshal(map[string]any{
 				"added":   addCounts,
 				"removed": removeCounts,
 			})
🧹 Nitpick comments (1)
internal/services/dirscan/service.go (1)

511-553: Consider using context.Background() for getRunTimes call.

Line 529 uses context.Background() for UpdateRunCompleted to ensure completion even if the run context is canceled. However, line 536 passes ctx to getRunTimes. While this works because ctx.Err() != nil is checked earlier (lines 516-527), using context.Background() would be more consistent with the pattern established for post-completion operations.

This is a minor consistency point - the current code will work correctly since getRunTimes handles errors gracefully by returning nil timestamps.

♻️ Optional: Use consistent context for completion operations
-	startedAt, completedAt := s.getRunTimes(ctx, runID)
+	startedAt, completedAt := s.getRunTimes(context.Background(), runID)
 	s.notify(ctx, notifications.Event{

@s0up4200 s0up4200 merged commit 1b9aa9d into develop Feb 17, 2026
4 checks passed
@s0up4200 s0up4200 deleted the feat/notifications-automation-crossseed branch February 17, 2026 22:31
@coderabbitai coderabbitai bot mentioned this pull request Feb 21, 2026
alexlebens pushed a commit to alexlebens/infrastructure that referenced this pull request Feb 22, 2026
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/autobrr/qui](https://github.com/autobrr/qui) | minor | `v1.13.1` → `v1.14.0` |

---

### Release Notes

<details>
<summary>autobrr/qui (ghcr.io/autobrr/qui)</summary>

### [`v1.14.0`](https://github.com/autobrr/qui/releases/tag/v1.14.0)

[Compare Source](autobrr/qui@v1.13.1...v1.14.0)

#### Changelog

##### New Features

- [`6f8e6ed`](autobrr/qui@6f8e6ed): feat(api): add torrent field endpoint for select all copy ([#&#8203;1477](autobrr/qui#1477)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`2d9b4c7`](autobrr/qui@2d9b4c7): feat(automation): trigger external programs automatically via automation rules ([#&#8203;1284](autobrr/qui#1284)) ([@&#8203;0rkag](https://github.com/0rkag))
- [`32692a4`](autobrr/qui@32692a4): feat(automations): Add the ability to define the move automation with a templated path ([#&#8203;1376](autobrr/qui#1376)) ([@&#8203;ColinHebert](https://github.com/ColinHebert))
- [`61bbeb1`](autobrr/qui@61bbeb1): feat(automations): add Resume action to Automations ([#&#8203;1350](autobrr/qui#1350)) ([@&#8203;cy1der](https://github.com/cy1der))
- [`450b98f`](autobrr/qui@450b98f): feat(automations): grouping + release fields ([#&#8203;1467](autobrr/qui#1467)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`18d4a64`](autobrr/qui@18d4a64): feat(automations): match tracker conditions by display name ([#&#8203;1420](autobrr/qui#1420)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`7c67b82`](autobrr/qui@7c67b82): feat(automations): show activity run details ([#&#8203;1385](autobrr/qui#1385)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`177ef4d`](autobrr/qui@177ef4d): feat(crossseed): Multiple hard/reflink dirs ([#&#8203;1289](autobrr/qui#1289)) ([@&#8203;rybertm](https://github.com/rybertm))
- [`a72b673`](autobrr/qui@a72b673): feat(crossseed): gazelle-only OPS/RED ([#&#8203;1436](autobrr/qui#1436)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`6a29384`](autobrr/qui@6a29384): feat(crossseed): match bit depth ([#&#8203;1427](autobrr/qui#1427)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`c7fd5aa`](autobrr/qui@c7fd5aa): feat(dirscan): add max searchee age filter ([#&#8203;1486](autobrr/qui#1486)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`d595a55`](autobrr/qui@d595a55): feat(documentation): add AI doc actions and llms discoverability ([#&#8203;1451](autobrr/qui#1451)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`562ab3f`](autobrr/qui@562ab3f): feat(metrics): add tracker metrics ([#&#8203;1073](autobrr/qui#1073)) ([@&#8203;Winter](https://github.com/Winter))
- [`1b9aa9d`](autobrr/qui@1b9aa9d): feat(notifications): add shoutrrr and notifiarr ([#&#8203;1371](autobrr/qui#1371)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`6d1dac7`](autobrr/qui@6d1dac7): feat(pwa): add protocol and file handlers for magnet links and torrent files ([#&#8203;783](autobrr/qui#783)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`42fa501`](autobrr/qui@42fa501): feat(torrents): add unified cross-instance torrent table ([#&#8203;1481](autobrr/qui#1481)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`498eaca`](autobrr/qui@498eaca): feat(ui): show speeds in page title ([#&#8203;1292](autobrr/qui#1292)) ([@&#8203;NoLife141](https://github.com/NoLife141))
- [`94a506e`](autobrr/qui@94a506e): feat(unregistered): nem talalhato ([#&#8203;1483](autobrr/qui#1483)) ([@&#8203;KyleSanderson](https://github.com/KyleSanderson))
- [`8bf366c`](autobrr/qui@8bf366c): feat(web): add logs nav ([#&#8203;1458](autobrr/qui#1458)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`babc88d`](autobrr/qui@babc88d): feat(web): add responsive popover with mobile drawer support ([#&#8203;1398](autobrr/qui#1398)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`06d341b`](autobrr/qui@06d341b): feat(web): add torrent table selection quick wins ([#&#8203;1455](autobrr/qui#1455)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`56fbbec`](autobrr/qui@56fbbec): feat(web): hide selection column ([#&#8203;1460](autobrr/qui#1460)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`46814aa`](autobrr/qui@46814aa): feat(web): qBittorrent autorun preferences ([#&#8203;1430](autobrr/qui#1430)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`342643e`](autobrr/qui@342643e): feat(web): unify instance settings & qbit options dialog ([#&#8203;1257](autobrr/qui#1257)) ([@&#8203;0rkag](https://github.com/0rkag))
- [`e634d01`](autobrr/qui@e634d01): feat: add cross-seed blocklist ([#&#8203;1391](autobrr/qui#1391)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`13aaac8`](autobrr/qui@13aaac8): feat: add dry-run workflows ([#&#8203;1395](autobrr/qui#1395)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`f01101d`](autobrr/qui@f01101d): feat: add option to disable built-in authentication ([#&#8203;1464](autobrr/qui#1464)) ([@&#8203;libussa](https://github.com/libussa))
- [`6d1da50`](autobrr/qui@6d1da50): feat: download individual content files from context menu ([#&#8203;1465](autobrr/qui#1465)) ([@&#8203;libussa](https://github.com/libussa))
- [`77e9abf`](autobrr/qui@77e9abf): feat: migrate to dodopayments ([#&#8203;1407](autobrr/qui#1407)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`9f6c856`](autobrr/qui@9f6c856): feat: support basic auth for ARR and Torznab ([#&#8203;1442](autobrr/qui#1442)) ([@&#8203;s0up4200](https://github.com/s0up4200))

##### Bug Fixes

- [`8a06d4b`](autobrr/qui@8a06d4b): fix(api): correct add-torrent OpenAPI param names and add missing fields ([#&#8203;1426](autobrr/qui#1426)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`b9a687c`](autobrr/qui@b9a687c): fix(api): honor explicit basic auth clear from URL userinfo ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`948ca67`](autobrr/qui@948ca67): fix(api): tighten CORS/auth routing and base URL joins ([#&#8203;1325](autobrr/qui#1325)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`12bea13`](autobrr/qui@12bea13): fix(automations): improve applied action summaries ([#&#8203;1478](autobrr/qui#1478)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`8fe658b`](autobrr/qui@8fe658b): fix(automations): negate regex match for NotContains/NotEqual operators ([#&#8203;1441](autobrr/qui#1441)) ([@&#8203;andresatierf](https://github.com/andresatierf))
- [`8a808eb`](autobrr/qui@8a808eb): fix(automations): respect remove-only tag conditions ([#&#8203;1444](autobrr/qui#1444)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`a72715e`](autobrr/qui@a72715e): fix(backups): add failure cooldown and export throttling ([#&#8203;1214](autobrr/qui#1214)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`2e75c14`](autobrr/qui@2e75c14): fix(backups): skip exports missing metadata ([#&#8203;1362](autobrr/qui#1362)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`5658421`](autobrr/qui@5658421): fix(config): update commented log settings in place ([#&#8203;1402](autobrr/qui#1402)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`62c50c0`](autobrr/qui@62c50c0): fix(crossseed): tighten TV title matching ([#&#8203;1445](autobrr/qui#1445)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`e7cc489`](autobrr/qui@e7cc489): fix(dirscan): prevent immediate requeue after cancel ([#&#8203;1446](autobrr/qui#1446)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`36cbfcf`](autobrr/qui@36cbfcf): fix(docs): avoid mdx jsx parse error ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`d8d6f62`](autobrr/qui@d8d6f62): fix(filters): stabilize dense sidebar layout ([#&#8203;1384](autobrr/qui#1384)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`b959fc6`](autobrr/qui@b959fc6): fix(orphanscan): NFC-normalize paths to avoid false orphans ([#&#8203;1422](autobrr/qui#1422)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`598e994`](autobrr/qui@598e994): fix(reflink): retry EAGAIN clones ([#&#8203;1360](autobrr/qui#1360)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`aaa5ee0`](autobrr/qui@aaa5ee0): fix(reflinktree): retry transient FICLONE EINVAL and add diagnostics ([#&#8203;1487](autobrr/qui#1487)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`647af31`](autobrr/qui@647af31): fix(rss): enable rules list scrolling ([#&#8203;1359](autobrr/qui#1359)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`c356a6f`](autobrr/qui@c356a6f): fix(sync): Optimize torrent sorting and reference management ([#&#8203;1474](autobrr/qui#1474)) ([@&#8203;KyleSanderson](https://github.com/KyleSanderson))
- [`cf4310e`](autobrr/qui@cf4310e): fix(ui): update placeholder text in ArrInstanceForm based on instance type ([#&#8203;1375](autobrr/qui#1375)) ([@&#8203;pashioya](https://github.com/pashioya))
- [`92b6748`](autobrr/qui@92b6748): fix(web): format IPv6 peer addresses and copy IP without port ([#&#8203;1417](autobrr/qui#1417)) ([@&#8203;sleepm](https://github.com/sleepm))
- [`25039bc`](autobrr/qui@25039bc): fix(web): handle SSO session expiry behind Cloudflare Access and other proxies ([#&#8203;1438](autobrr/qui#1438)) ([@&#8203;nitrobass24](https://github.com/nitrobass24))
- [`77fe310`](autobrr/qui@77fe310): fix(web): prevent category submenu re-render ([#&#8203;1357](autobrr/qui#1357)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`a42ab1e`](autobrr/qui@a42ab1e): fix(web): raise instance preferences max value from 999 to 99999 ([#&#8203;1311](autobrr/qui#1311)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`540168c`](autobrr/qui@540168c): fix(web): raise virtualization threshold ([#&#8203;1355](autobrr/qui#1355)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`8547dc6`](autobrr/qui@8547dc6): fix(web): remove column filters when column is hidden ([#&#8203;1418](autobrr/qui#1418)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`6b09b8d`](autobrr/qui@6b09b8d): fix(web): remove panel size bounds ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`db4cdc4`](autobrr/qui@db4cdc4): fix(web): show piece size in torrent details ([#&#8203;1365](autobrr/qui#1365)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`1f94a06`](autobrr/qui@1f94a06): fix(web): use absolute for scroll-to-top on desktop ([#&#8203;1419](autobrr/qui#1419)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`e31fe3a`](autobrr/qui@e31fe3a): fix: detect tracker health support after qBit upgrade ([#&#8203;909](autobrr/qui#909)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`52f01da`](autobrr/qui@52f01da): fix: disable update indicators when update checks are off ([#&#8203;1364](autobrr/qui#1364)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`f7e3fed`](autobrr/qui@f7e3fed): fix: normalize DD+ and DDP file keys ([#&#8203;1456](autobrr/qui#1456)) ([@&#8203;s0up4200](https://github.com/s0up4200))

##### Other Changes

- [`d914301`](autobrr/qui@d914301): chore(ci): fire Blacksmith (my wallet screamed) ([#&#8203;1408](autobrr/qui#1408)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`b43327d`](autobrr/qui@b43327d): chore(deps): bump the golang group with 2 updates ([#&#8203;1378](autobrr/qui#1378)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`57747bd`](autobrr/qui@57747bd): chore(deps): bump the npm group across 1 directory with 27 updates ([#&#8203;1379](autobrr/qui#1379)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`a43850d`](autobrr/qui@a43850d): chore(docs): add BIMI SVG logo ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`914bede`](autobrr/qui@914bede): chore(funding): add Patreon to FUNDING.yml ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`8b76f1e`](autobrr/qui@8b76f1e): docs(automations): clarify tag matching examples ([#&#8203;1457](autobrr/qui#1457)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`2994054`](autobrr/qui@2994054): docs(readme): restore concise README ([#&#8203;1452](autobrr/qui#1452)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`51237d4`](autobrr/qui@51237d4): docs: Add configuration reference ([#&#8203;1440](autobrr/qui#1440)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`741462c`](autobrr/qui@741462c): docs: add Windows installation guide ([#&#8203;1463](autobrr/qui#1463)) ([@&#8203;soggy-cr0uton](https://github.com/soggy-cr0uton))
- [`6a11430`](autobrr/qui@6a11430): docs: clarify autobrr filter + apply troubleshooting ([#&#8203;1459](autobrr/qui#1459)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`5a2edc2`](autobrr/qui@5a2edc2): docs: update 2 documentation files ([#&#8203;1454](autobrr/qui#1454)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`139ada9`](autobrr/qui@139ada9): docs: update contributing.md ([#&#8203;1470](autobrr/qui#1470)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`3909aa1`](autobrr/qui@3909aa1): docs: update docs/features/automations.md ([#&#8203;1447](autobrr/qui#1447)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`5dc57ca`](autobrr/qui@5dc57ca): docs: update intro.md ([#&#8203;1453](autobrr/qui#1453)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`5d9e986`](autobrr/qui@5d9e986): perf(web): memoize useDateTimeFormatters ([#&#8203;1403](autobrr/qui#1403)) ([@&#8203;jabloink](https://github.com/jabloink))

**Full Changelog**: <autobrr/qui@v1.13.1...v1.14.0>

#### Docker images

- `docker pull ghcr.io/autobrr/qui:v1.14.0`
- `docker pull ghcr.io/autobrr/qui:latest`

#### What to do next?

- Join our [Discord server](https://discord.autobrr.com/qui)

Thank you for using qui!

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNS43IiwidXBkYXRlZEluVmVyIjoiNDMuMjUuNyIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW1hZ2UiXX0=-->

Reviewed-on: https://gitea.alexlebens.dev/alexlebens/infrastructure/pulls/4154
Co-authored-by: Renovate Bot <renovate-bot@alexlebens.net>
Co-committed-by: Renovate Bot <renovate-bot@alexlebens.net>
@ppkhoa
Copy link
Copy Markdown

ppkhoa commented Mar 2, 2026

Can you make Discord notification (or notifications in general) content customizable? As of the latest develop docker image, this is the notification from qui via Discord:

image

I also check notification via smartwatch and all I see is Torrent Added and filename, which is not very clear which instance and the status, either just a cross-seed added (progress 1.00 or 100%) or a new torrent, had to pull out the phone and open Discord to check. Instead of catering to my specific use case, maybe give the users option to customize what the notification look like?

Also, another note, if progress is 1.00, meaning it's already completed => it's a cross-seed, I don't need the other fields like ETA, ratio, amount left, etc..., not useful for notification, which only display static info. Just Instance, Filename, Tracker and Tags are sufficient since those info are static. Not sure if other users agree, just my opinion

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants