- notifier: show artist display names (not internal IDs) in digest; resolve names from artist_settings and fall back to ID when unavailable - notifier: skip sending an empty digest to avoid daily spam - config: require telegram token/chat_id when enabled - web: warn loudly when auth is disabled on a non-loopback bind; add HTTP server timeouts - web: treat SetReleaseIgnored "release not found" as benign redirect (0 rows) - musicbrainz: reject low-score/name-mismatched MBID resolutions instead of silently caching the wrong artist - database: remove dead duplicate err check; harden DSN param appending - musicbrainz: check rows.Err() after iterating existing releases
72 lines
2.3 KiB
Go
72 lines
2.3 KiB
Go
package notifier
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"naviwatcher/internal/scanner"
|
|
)
|
|
|
|
// FormatDigest renders newly-found missing releases into a human-readable
|
|
// Telegram message grouped by artist, with per-artist counts and a link to
|
|
// the Web UI dashboard. It is deterministic: artists are sorted by label and
|
|
// releases within an artist are sorted by title.
|
|
//
|
|
// artistNames maps an ArtistID to its human-readable display name. Names are
|
|
// optional: if an ID is absent from the map (or the map itself is nil), the
|
|
// raw ArtistID is used as the label so the digest remains informative.
|
|
func FormatDigest(missing []scanner.MissingRelease, uiBaseURL string, artistNames map[string]string) string {
|
|
if len(missing) == 0 {
|
|
return "NaviWatcher: no new missing releases found."
|
|
}
|
|
|
|
type entry struct {
|
|
title string
|
|
}
|
|
byArtist := make(map[string][]entry)
|
|
order := make([]string, 0)
|
|
for _, r := range missing {
|
|
if _, ok := byArtist[r.ArtistID]; !ok {
|
|
order = append(order, r.ArtistID)
|
|
}
|
|
byArtist[r.ArtistID] = append(byArtist[r.ArtistID], entry{title: r.Title})
|
|
}
|
|
// Stable ordering by display label (name if known, else ID).
|
|
sort.Slice(order, func(i, j int) bool {
|
|
return artistLabel(order[i], artistNames) < artistLabel(order[j], artistNames)
|
|
})
|
|
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "NaviWatcher: %d new missing release(s) found:\n\n", len(missing))
|
|
for _, artistID := range order {
|
|
entries := byArtist[artistID]
|
|
titles := make([]string, 0, len(entries))
|
|
for _, e := range entries {
|
|
titles = append(titles, e.title)
|
|
}
|
|
sort.Strings(titles)
|
|
fmt.Fprintf(&b, "%s (%d):\n", artistLabel(artistID, artistNames), len(titles))
|
|
for _, t := range titles {
|
|
fmt.Fprintf(&b, " - %s\n", t)
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
if uiBaseURL != "" {
|
|
fmt.Fprintf(&b, "View details: %s\n", strings.TrimRight(uiBaseURL, "/"))
|
|
}
|
|
return strings.TrimRight(b.String(), "\n")
|
|
}
|
|
|
|
// artistLabel returns the human-readable display name for an artist ID when
|
|
// available, otherwise the raw ID. A non-empty name takes precedence so
|
|
// operators see recognizable artist names rather than opaque internal IDs.
|
|
func artistLabel(artistID string, names map[string]string) string {
|
|
if names != nil {
|
|
if name, ok := names[artistID]; ok && name != "" {
|
|
return name
|
|
}
|
|
}
|
|
return artistID
|
|
}
|