package library import ( "context" "sync" "github.com/jackc/pgx/v5/pgtype" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" "git.fabledsword.com/bvandeusen/minstrel/internal/notifications" ) // Package-level notifier for library-health notices to admins (M489): a // scan that failed, tracks gone missing, new duplicates. Set once at startup // beside SetEventBus, for the same reason the bus is package-level. Nil, as in // tests that never set it, means nobody is told. var ( notifierMu sync.RWMutex notifier *notifications.Notifier ) // SetNotifier wires the notifications inbox into the library package. func SetNotifier(n *notifications.Notifier) { notifierMu.Lock() defer notifierMu.Unlock() notifier = n } // notifyAdmins tells every admin about a library-health event. These kinds // coalesce, so a burst is one unread notice with a running count. func notifyAdmins(ctx context.Context, kind notifications.Kind, p notifications.Payload) { notifierMu.RLock() n := notifier notifierMu.RUnlock() n.NotifyLogged(ctx, kind, notifications.ToAdmins(pgtype.UUID{}), p.Map()) } // notifyItems is how many names a notice carries; the notifications package // keeps the newest ten across a coalesced burst. const notifyItems = 10 // TrackLabel names a track as a notice does: "Artist – Title", or the title // alone when the artist is unknown. func TrackLabel(artist, title string) string { if artist == "" { return title } return artist + " – " + title } // notifyStillMissing tells admins which of the tracks a scan marked missing // are still gone after it adopted moves and folded replacements (M542). A // marked track the scan healed is not news: reporting it is what made a // quality upgrade read as lost music. func (s *Scanner) notifyStillMissing(ctx context.Context, marked []pgtype.UUID) { if len(marked) == 0 || ctx.Err() != nil { return } rows, err := dbq.New(s.pool).ListStillMissingTrackLabels(ctx, marked) if err != nil { s.logger.Warn("library scan: listing missing tracks for the notice failed", "err", err) return } if len(rows) == 0 { return } items := make([]string, 0, min(len(rows), notifyItems)) for _, r := range rows[:min(len(rows), notifyItems)] { items = append(items, TrackLabel(r.ArtistName, r.Title)) } notifyAdmins(ctx, notifications.KindTracksMissing, notifications.Payload{Count: int64(len(rows)), Items: items}) } // notifyScanFinished tells admins a scan run ended in error. A scan cut short // by shutdown is not a failure anyone needs telling about. func notifyScanFinished(ctx context.Context, errMsg string) { if errMsg == "" || ctx.Err() != nil { return } notifyAdmins(ctx, notifications.KindScanFailed, notifications.Payload{Count: 1, Detail: errMsg}) }