package notifications import ( "bytes" "embed" "encoding/json" "fmt" htmltemplate "html/template" "strings" texttemplate "text/template" ) //go:embed templates/digest.txt templates/digest.html var digestFS embed.FS var ( digestText = texttemplate.Must(texttemplate.ParseFS(digestFS, "templates/digest.txt")) digestHTML = htmltemplate.Must(htmltemplate.ParseFS(digestFS, "templates/digest.html")) ) // digestEmail is one rendered email. type digestEmail struct { Subject string Text string HTML string } type digestLine struct { Title string Body string URL string } type digestArtist struct { Name string URL string Titles []digestLine } type digestVars struct { Name string Intro string Items []digestLine Artists []digestArtist SettingsURL string } // renderDigest renders one group's email. base is the public address; with // none set, the email still goes out, without links. func renderDigest(group EmailGroup, name string, items []pendingItem, base string) (digestEmail, error) { vars := digestVars{Name: name, SettingsURL: joinURL(base, "/settings#notifications")} var subject string if group == EmailSummary { vars.Artists = summaryArtists(items, base) vars.Intro = "New in your library since the last summary:" subject = "Minstrel: new music in your library" } else { for _, it := range items { r := Render(it.Kind, it.Payload) vars.Items = append(vars.Items, digestLine{Title: r.Title, Body: r.Body, URL: joinURL(base, r.Link)}) } vars.Intro = "Here's what happened on Minstrel:" subject = fmt.Sprintf("Minstrel: %d updates", len(items)) if len(items) == 1 { subject = "Minstrel: " + vars.Items[0].Title } } var text, html bytes.Buffer if err := digestText.Execute(&text, vars); err != nil { return digestEmail{}, fmt.Errorf("render text: %w", err) } if err := digestHTML.Execute(&html, vars); err != nil { return digestEmail{}, fmt.Errorf("render html: %w", err) } return digestEmail{Subject: subject, Text: text.String(), HTML: html.String()}, nil } // summaryArtists groups new-music arrivals by artist, in the order each // artist first arrived, with each artist's albums and tracks under them. An // artist request has no title of its own; the artist links to its page. func summaryArtists(items []pendingItem, base string) []digestArtist { var out []digestArtist index := map[string]int{} for _, it := range items { var p Payload _ = json.Unmarshal(it.Payload, &p) artist, title := p.Artist, p.Title if artist == "" { // Rows from before Artist was recorded carry only "Artist – Title". artist, title, _ = strings.Cut(p.Name, " – ") } if artist == "" { artist = "Something you asked for" } i, ok := index[artist] if !ok { i = len(out) index[artist] = i out = append(out, digestArtist{Name: artist}) } link := Render(it.Kind, it.Payload).Link if title == "" { if p.ArtistID != "" { out[i].URL = joinURL(base, link) } continue } out[i].Titles = append(out[i].Titles, digestLine{Title: title, URL: joinURL(base, link)}) } return out } // joinURL is base followed by path, or "" when no public address is set. func joinURL(base, path string) string { if base == "" || path == "" { return "" } return strings.TrimRight(base, "/") + path }