| 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 | } |