This commit is contained in:
ponzischeme89
2026-08-11 23:41:10 +12:00
parent 0e183cf560
commit 5fed3360c2
42 changed files with 1340 additions and 130 deletions
+131
View File
@@ -0,0 +1,131 @@
package api
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/ponzischeme89/memby/server/internal/sonarr"
"github.com/ponzischeme89/memby/server/internal/store"
)
// WatchSonarrLifecycle seeds the durable status history on startup, then refreshes it
// daily. History stores changes rather than identical daily snapshots: it still records
// the complete lifecycle while making an active-to-cancelled transition unambiguous.
func (s *Server) WatchSonarrLifecycle(ctx context.Context, interval time.Duration) {
if s.sonarr == nil || interval <= 0 {
return
}
scan := func() {
if err := s.scanSonarrLifecycle(ctx); err != nil && ctx.Err() == nil {
s.log.Warn("Sonarr lifecycle scan failed", "error", err)
}
}
scan()
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
scan()
}
}
}
func (s *Server) scanSonarrLifecycle(ctx context.Context) error {
series, err := s.sonarr.Series(ctx)
if err != nil {
return fmt.Errorf("read Sonarr series: %w", err)
}
now := time.Now().UTC()
observations := make([]store.SonarrSeriesStatus, 0, len(series))
for _, item := range series {
key := sonarrSeriesStatusKey(item)
if key == "" || strings.TrimSpace(item.Status) == "" {
continue
}
observations = append(observations, store.SonarrSeriesStatus{
SeriesKey: key, SonarrSeriesID: item.ID, TVDBID: item.TVDBID,
Title: item.Title, Year: item.Year, Status: item.Status, ObservedAt: now,
})
}
changes, err := s.store.RecordSonarrSeriesStatuses(ctx, observations)
if err != nil {
return err
}
cancellations := make([]store.SonarrSeriesStatusChange, 0, len(changes))
for _, change := range changes {
if sonarrBecameCancelled(change.PreviousStatus, change.Current.Status) {
cancellations = append(cancellations, change)
}
}
if len(cancellations) == 0 {
s.log.Info("Sonarr lifecycle scan complete", "series", len(observations), "changes", len(changes))
return nil
}
users, err := s.store.KnownUsers(ctx)
if err != nil {
return err
}
preferences := map[string]store.NotificationPreferences{}
preferenceErrors := map[string]bool{}
notifications := 0
for _, change := range cancellations {
for _, user := range users {
prefs, ok := preferences[user.ID]
if !ok && !preferenceErrors[user.ID] {
prefs, err = s.store.NotificationPreferences(ctx, user.ID)
if err != nil {
preferenceErrors[user.ID] = true
s.log.Warn("notification preferences unavailable during Sonarr lifecycle scan",
"user", user.ID, "error", err)
continue
}
preferences[user.ID] = prefs
}
if preferenceErrors[user.ID] || !prefs.Enabled {
continue
}
eventAt := change.Current.ObservedAt
sourceKey := fmt.Sprintf("show-cancelled:%s:%d", change.Current.SeriesKey, change.HistoryID)
message := change.Current.Title + " is now listed as cancelled in Sonarr."
if err := s.store.UpsertNotification(
ctx, user.ID, sourceKey, "show-cancelled", "",
"Show cancelled", message, &eventAt,
); err != nil {
s.log.Warn("Sonarr cancellation notification failed",
"user", user.ID, "show", change.Current.Title, "error", err)
continue
}
notifications++
}
}
s.log.Info("Sonarr lifecycle scan complete",
"series", len(observations), "changes", len(changes),
"cancelled", len(cancellations), "notifications", notifications)
return nil
}
func sonarrSeriesStatusKey(series sonarr.Series) string {
if series.TVDBID > 0 {
return "tvdb:" + strconv.Itoa(series.TVDBID)
}
if series.ID > 0 {
return "sonarr:" + strconv.Itoa(series.ID)
}
return ""
}
func sonarrBecameCancelled(previous, current string) bool {
previous = strings.ToLower(strings.TrimSpace(previous))
current = strings.ToLower(strings.TrimSpace(current))
active := previous == "continuing" || previous == "upcoming"
cancelled := current == "ended" || current == "deleted" ||
current == "cancelled" || current == "canceled"
return active && cancelled
}