feat: add opml query to feeds-go 5e24777c
Steve Simkins · 2026-05-20 14:45 3 file(s) · +66 −2
apps/feeds-go/feeds.go +4 −1
169 169
	return html.UnescapeString(b.String())
170 170
}
171 171
172 -
func previewURLs(ctx context.Context, urls []string, log *slog.Logger) []FeedPreviewItem {
172 +
func previewURLs(ctx context.Context, urls []string, perFeed int, log *slog.Logger) []FeedPreviewItem {
173 173
	var wg sync.WaitGroup
174 174
	var mu sync.Mutex
175 175
	items := []FeedPreviewItem{}
189 189
			feedTitle := res.Title
190 190
			local := make([]FeedPreviewItem, 0, len(res.Entries))
191 191
			for _, entry := range res.Entries {
192 +
				if perFeed > 0 && len(local) >= perFeed {
193 +
					break
194 +
				}
192 195
				author := feedTitle
193 196
				if entry.Author != "" && feedTitle != "" {
194 197
					author = feedTitle + " - " + entry.Author
apps/feeds-go/handlers_public.go +37 −1
4 4
	"encoding/xml"
5 5
	"fmt"
6 6
	"net/http"
7 +
	"net/url"
7 8
	"strings"
8 9
	"time"
9 10
10 11
	"github.com/stevedylandev/andromeda/crates-go/web"
11 12
)
13 +
14 +
const opmlPreviewPerFeed = 3
12 15
13 16
func (a *App) indexHandler(w http.ResponseWriter, r *http.Request) {
14 17
	query := r.URL.Query().Get("url")
24 27
			web.Render(a.Templates, w, "index.html", data, a.Log)
25 28
			return
26 29
		}
27 -
		for _, item := range previewURLs(r.Context(), urls, a.Log) {
30 +
		perFeed := 0
31 +
		if len(urls) == 1 && isOPMLURL(urls[0]) {
32 +
			content, err := fetchOPMLDoc(r.Context(), urls[0])
33 +
			if err != nil {
34 +
				a.Log.Warn("opml preview fetch failed", "url", urls[0], "err", err)
35 +
				data.Error = "Could not load OPML"
36 +
				web.Render(a.Templates, w, "index.html", data, a.Log)
37 +
				return
38 +
			}
39 +
			entries := parseOPML(content)
40 +
			if len(entries) == 0 {
41 +
				data.Error = "No feeds found in OPML"
42 +
				web.Render(a.Templates, w, "index.html", data, a.Log)
43 +
				return
44 +
			}
45 +
			feedURLs := make([]string, 0, len(entries))
46 +
			for _, e := range entries {
47 +
				if e.XMLURL != "" {
48 +
					feedURLs = append(feedURLs, e.XMLURL)
49 +
				}
50 +
			}
51 +
			urls = feedURLs
52 +
			data.FeedURLs = feedURLs
53 +
			perFeed = opmlPreviewPerFeed
54 +
		}
55 +
		for _, item := range previewURLs(r.Context(), urls, perFeed, a.Log) {
28 56
			data.Items = append(data.Items, templateItem{Title: item.Title, Link: item.Link, Author: item.Author, FormattedDate: formatDate(item.Published)})
29 57
		}
30 58
		web.Render(a.Templates, w, "index.html", data, a.Log)
196 224
	_, _ = w.Write([]byte(xml.Header))
197 225
	_, _ = w.Write(body)
198 226
}
227 +
228 +
func isOPMLURL(raw string) bool {
229 +
	u, err := url.Parse(raw)
230 +
	if err != nil {
231 +
		return false
232 +
	}
233 +
	return strings.HasSuffix(strings.ToLower(u.Path), ".opml")
234 +
}
apps/feeds-go/opml.go +25 −0
1 1
package main
2 2
3 3
import (
4 +
	"context"
4 5
	"encoding/xml"
6 +
	"fmt"
7 +
	"io"
8 +
	"net/http"
5 9
	"strings"
6 10
)
7 11
48 52
	walk(doc.Body.Nodes, "")
49 53
	return out
50 54
}
55 +
56 +
func fetchOPMLDoc(ctx context.Context, opmlURL string) (string, error) {
57 +
	client := buildHTTPClient()
58 +
	req, err := newRequest(ctx, http.MethodGet, opmlURL)
59 +
	if err != nil {
60 +
		return "", err
61 +
	}
62 +
	resp, err := client.Do(req)
63 +
	if err != nil {
64 +
		return "", fmt.Errorf("fetch failed: %w", err)
65 +
	}
66 +
	defer resp.Body.Close()
67 +
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
68 +
		return "", fmt.Errorf("upstream returned %d", resp.StatusCode)
69 +
	}
70 +
	body, err := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
71 +
	if err != nil {
72 +
		return "", fmt.Errorf("read failed: %w", err)
73 +
	}
74 +
	return string(body), nil
75 +
}