diff options
| author | Sam Scholten | 2026-08-30 12:50:24 +1000 |
|---|---|---|
| committer | Sam Scholten | 2026-08-30 12:52:39 +1000 |
| commit | 96ab2fcb2ff442698465389d75390afa91629165 (patch) | |
| tree | b36ef45dccaa791b0fce4dcad99d564635663c10 /cmds/rss.go | |
| download | fluxrec-96ab2fcb2ff442698465389d75390afa91629165.tar.gz fluxrec-96ab2fcb2ff442698465389d75390afa91629165.zip | |
fluxrec: personal Miniflux article recommender
Diffstat (limited to 'cmds/rss.go')
| -rw-r--r-- | cmds/rss.go | 82 |
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 +} |
