apps/feeds/opml.go 1.8 K raw
1
package main
2
3
import (
4
	"context"
5
	"encoding/xml"
6
	"fmt"
7
	"io"
8
	"net/http"
9
	"strings"
10
)
11
12
type OPMLEntry struct {
13
	XMLURL   string
14
	Title    string
15
	HTMLURL  string
16
	Category string
17
}
18
19
func parseOPML(content string) []OPMLEntry {
20
	dec := xml.NewDecoder(strings.NewReader(content))
21
	type outline struct {
22
		Title   string    `xml:"title,attr"`
23
		Text    string    `xml:"text,attr"`
24
		XMLURL  string    `xml:"xmlUrl,attr"`
25
		HTMLURL string    `xml:"htmlUrl,attr"`
26
		Nodes   []outline `xml:"outline"`
27
	}
28
	type opml struct {
29
		Body struct {
30
			Nodes []outline `xml:"outline"`
31
		} `xml:"body"`
32
	}
33
	var doc opml
34
	if err := dec.Decode(&doc); err != nil {
35
		return nil
36
	}
37
	var out []OPMLEntry
38
	var walk func(nodes []outline, category string)
39
	walk = func(nodes []outline, category string) {
40
		for _, node := range nodes {
41
			title := firstNonEmpty(node.Title, node.Text)
42
			if strings.TrimSpace(node.XMLURL) != "" {
43
				out = append(out, OPMLEntry{XMLURL: strings.TrimSpace(node.XMLURL), Title: title, HTMLURL: strings.TrimSpace(node.HTMLURL), Category: strings.TrimSpace(category)})
44
				if len(node.Nodes) > 0 {
45
					walk(node.Nodes, title)
46
				}
47
				continue
48
			}
49
			walk(node.Nodes, title)
50
		}
51
	}
52
	walk(doc.Body.Nodes, "")
53
	return out
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
}