aboutsummaryrefslogtreecommitdiff
path: root/cmds/rss.go
diff options
context:
space:
mode:
authorSam Scholten2026-08-30 12:50:24 +1000
committerSam Scholten2026-08-30 12:52:39 +1000
commit96ab2fcb2ff442698465389d75390afa91629165 (patch)
treeb36ef45dccaa791b0fce4dcad99d564635663c10 /cmds/rss.go
downloadfluxrec-96ab2fcb2ff442698465389d75390afa91629165.tar.gz
fluxrec-96ab2fcb2ff442698465389d75390afa91629165.zip
fluxrec: personal Miniflux article recommender
Diffstat (limited to 'cmds/rss.go')
-rw-r--r--cmds/rss.go82
1 files changed, 82 insertions, 0 deletions
diff --git a/cmds/rss.go b/cmds/rss.go
new file mode 100644
index 0000000..3c49513
--- /dev/null
+++ b/cmds/rss.go
@@ -0,0 +1,82 @@
+// RSS rendering for the recommendations feed: hand-rolled RSS 2.0 via
+// encoding/xml, no feed library. The render source is the runs.jsonl tail;
+// items appear newest batch first, kept order within a batch. Guid embeds
+// the original Miniflux entry ID (mf:<id>) for traceability.
+package cmds
+
+import (
+ "encoding/xml"
+ "fmt"
+ "time"
+
+ "fluxrec/core"
+)
+
+const rssXMLHeader = `<?xml version="1.0" encoding="UTF-8"?>` + "\n"
+
+type rssGUID struct {
+ IsPermaLink bool `xml:"isPermaLink,attr"`
+ Value string `xml:",chardata"`
+}
+
+type rssItem struct {
+ Title string `xml:"title"`
+ Link string `xml:"link"`
+ GUID rssGUID `xml:"guid"`
+ PubDate string `xml:"pubDate"`
+ Description string `xml:"description"`
+}
+
+type rssChannel struct {
+ Title string `xml:"title"`
+ Description string `xml:"description"`
+ Items []rssItem `xml:"item"`
+}
+
+type rssDocument struct {
+ XMLName xml.Name `xml:"rss"`
+ Version string `xml:"version,attr"`
+ Channel rssChannel `xml:"channel"`
+}
+
+// RenderRSS renders runs (oldest to newest, as read from runs.jsonl) as an
+// RSS 2.0 document. The source feed title prefixes each item description so
+// provenance is visible in-reader; item content was already cleaned when
+// the run row was written.
+func RenderRSS(runs []core.RunRow) ([]byte, error) {
+ doc := rssDocument{
+ Version: "2.0",
+ Channel: rssChannel{
+ Title: "fluxrec recommendations",
+ Description: "Top-scored Miniflux entries, refreshed on a schedule.",
+ },
+ }
+ for i := len(runs) - 1; i >= 0; i-- {
+ for _, k := range runs[i].Kept {
+ doc.Channel.Items = append(doc.Channel.Items, rssItem{
+ Title: k.Title,
+ Link: k.URL,
+ GUID: rssGUID{IsPermaLink: false, Value: fmt.Sprintf("mf:%d", k.EntryID)},
+ PubDate: k.PublishedAt.Format(time.RFC1123Z),
+ Description: rssDescription(k),
+ })
+ }
+ }
+ body, err := xml.MarshalIndent(doc, "", " ")
+ if err != nil {
+ return nil, fmt.Errorf("marshaling RSS: %w", err)
+ }
+ return append([]byte(rssXMLHeader), body...), nil
+}
+
+// rssDescription builds the item body: source feed name on the first line,
+// then the cleaned content (if any).
+func rssDescription(k core.KeptItem) string {
+ if k.Content == "" {
+ return k.FeedTitle
+ }
+ if k.FeedTitle == "" {
+ return k.Content
+ }
+ return k.FeedTitle + "\n\n" + k.Content
+}