feat: add opml query to feeds-go
5e24777c
3 file(s) · +66 −2
| 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 |
|
| 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 | + | } |
|
| 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 | + | } |
|