fix: address code review findings
This commit is contained in:
@@ -11,71 +11,94 @@ import (
|
||||
"naviwatcher/internal/scanner"
|
||||
)
|
||||
|
||||
// NotifyOnce queries for releases that have not yet been notified, builds a
|
||||
// digest, sends it through the given Sender, and marks each release as sent.
|
||||
// Releases already present in notifications_sent are excluded upstream by
|
||||
// GetUnnotifiedReleases, so this is idempotent across runs.
|
||||
// NotifyOnce computes the releases that are genuinely missing for the user,
|
||||
// intersects that set with the releases not yet notified, builds a digest of
|
||||
// the result, sends it through the given Sender, and marks each release as
|
||||
// sent.
|
||||
//
|
||||
// If there are no unnotified releases nothing is sent (the caller's scheduler
|
||||
// is responsible for not spamming the operator with an empty digest). When
|
||||
// releases are present, each is marked sent so a subsequent run will not
|
||||
// re-notify it.
|
||||
// "Missing" is the authoritative definition produced by the scanner: an
|
||||
// external release with no sufficiently similar local album (see
|
||||
// scanner.ScanAll). This intersection is what keeps the digest honest: a
|
||||
// release the user already owns in Navidrome must never be reported as a new
|
||||
// missing release, even though it still counts as "unnotified" on a fresh
|
||||
// database. Releases already in notifications_sent are excluded by
|
||||
// GetUnnotifiedReleases, so the call is idempotent across runs.
|
||||
//
|
||||
// threshold is the fuzzy-similarity cutoff passed through to the scanner; 0
|
||||
// selects the scanner's default. It must match config.Scanner.FuzzyThreshold
|
||||
// so the digest honors the operator's configured tolerance.
|
||||
//
|
||||
// If there are no newly-missing releases nothing is sent (the caller's
|
||||
// scheduler is responsible for not spamming the operator with an empty
|
||||
// digest). When releases are present, each is marked sent so a subsequent run
|
||||
// will not re-notify it.
|
||||
//
|
||||
// uiBaseURL is the externally-reachable base URL of the Web UI, appended to the
|
||||
// digest so operators can jump to the dashboard.
|
||||
func NotifyOnce(ctx context.Context, db *database.DB, sender Sender, cfg config.TelegramConfig, uiBaseURL string) (int, error) {
|
||||
func NotifyOnce(ctx context.Context, db *database.DB, sender Sender, cfg config.TelegramConfig, uiBaseURL string, threshold float64) (int, error) {
|
||||
if sender == nil {
|
||||
return 0, fmt.Errorf("notifier: sender must not be nil")
|
||||
}
|
||||
|
||||
// Authoritative missing set: external releases with no matching local album.
|
||||
// This is what the Web UI dashboard also shows, so the digest stays
|
||||
// consistent with what the operator sees in the UI.
|
||||
missing, err := scanner.ScanAll(ctx, db, threshold)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("notifier: scan missing releases: %w", err)
|
||||
}
|
||||
missingByRGID := make(map[string]scanner.MissingRelease, len(missing))
|
||||
for _, m := range missing {
|
||||
missingByRGID[m.RGID] = m
|
||||
}
|
||||
|
||||
// Restrict to releases not yet notified. A release that is genuinely missing
|
||||
// but was already announced is dropped here so it is never re-sent.
|
||||
unnotified, err := database.GetUnnotifiedReleases(db)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("notifier: query unnotified releases: %w", err)
|
||||
}
|
||||
|
||||
missing := make([]scanner.MissingRelease, 0, len(unnotified))
|
||||
toNotify := make([]scanner.MissingRelease, 0, len(unnotified))
|
||||
for _, r := range unnotified {
|
||||
missing = append(missing, scanner.MissingRelease{
|
||||
RGID: r.RGID,
|
||||
ArtistID: r.ArtistID,
|
||||
Title: r.Title,
|
||||
Type: r.Type,
|
||||
ReleaseDate: r.ReleaseDate,
|
||||
})
|
||||
if m, ok := missingByRGID[r.RGID]; ok {
|
||||
toNotify = append(toNotify, m)
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve human-readable artist names so the digest shows recognizable
|
||||
// labels instead of opaque internal artist IDs. A lookup failure for a
|
||||
// single artist must not abort the whole digest, so errors are ignored and
|
||||
// that artist falls back to its ID via artistLabel.
|
||||
names := make(map[string]string, len(missing))
|
||||
for _, r := range unnotified {
|
||||
if _, ok := names[r.ArtistID]; ok {
|
||||
names := make(map[string]string, len(toNotify))
|
||||
for _, m := range toNotify {
|
||||
if _, ok := names[m.ArtistID]; ok {
|
||||
continue
|
||||
}
|
||||
settings, err := database.GetArtistSettings(db, r.ArtistID)
|
||||
settings, err := database.GetArtistSettings(db, m.ArtistID)
|
||||
if err == nil && settings.Name != "" {
|
||||
names[r.ArtistID] = settings.Name
|
||||
names[m.ArtistID] = settings.Name
|
||||
}
|
||||
}
|
||||
|
||||
message := FormatDigest(missing, uiBaseURL, names)
|
||||
// Nothing to report: skip sending so the operator is not spammed with an
|
||||
// empty digest on every cron fire. The startup fire likewise stays quiet
|
||||
// until the first genuinely missing release appears.
|
||||
if len(unnotified) == 0 {
|
||||
if len(toNotify) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
message := FormatDigest(toNotify, uiBaseURL, names)
|
||||
if err := sender.Send(ctx, message); err != nil {
|
||||
return 0, fmt.Errorf("notifier: send digest: %w", err)
|
||||
}
|
||||
|
||||
for _, r := range unnotified {
|
||||
if err := database.MarkNotificationSent(db, r.RGID); err != nil {
|
||||
return 0, fmt.Errorf("notifier: mark sent for %s: %w", r.RGID, err)
|
||||
for _, m := range toNotify {
|
||||
if err := database.MarkNotificationSent(db, m.RGID); err != nil {
|
||||
return 0, fmt.Errorf("notifier: mark sent for %s: %w", m.RGID, err)
|
||||
}
|
||||
}
|
||||
return len(unnotified), nil
|
||||
return len(toNotify), nil
|
||||
}
|
||||
|
||||
// Schedule produces the next firing time strictly after the given time. It
|
||||
|
||||
Reference in New Issue
Block a user