Files
rsspipe/internal/opml/opml.go
T

158 lines
3.7 KiB
Go

package opml
import (
"encoding/xml"
"fmt"
"strings"
"time"
"github.com/haopengzhan/rsspipe/internal/models"
)
type OPML struct {
XMLName xml.Name `xml:"opml"`
Version string `xml:"version,attr"`
Head Head `xml:"head"`
Body Body `xml:"body"`
}
type Head struct {
Title string `xml:"title"`
DateCreated string `xml:"dateCreated,omitempty"`
DateModified string `xml:"dateModified,omitempty"`
}
type Body struct {
Outlines []Outline `xml:"outline"`
}
type Outline struct {
Text string `xml:"text,attr"`
Title string `xml:"title,attr,omitempty"`
Type string `xml:"type,attr,omitempty"`
XMLURL string `xml:"xmlUrl,attr,omitempty"`
HTMLURL string `xml:"htmlUrl,attr,omitempty"`
Outlines []Outline `xml:"outline,omitempty"`
}
type ImportedFeed struct {
Title string
FeedURL string
SiteURL string
CategoryTitle string
}
func ParseOPML(data []byte) ([]ImportedFeed, error) {
var doc OPML
if err := xml.Unmarshal(data, &doc); err != nil {
return nil, fmt.Errorf("failed to parse OPML XML: %w", err)
}
var feeds []ImportedFeed
for _, topOutline := range doc.Body.Outlines {
if topOutline.XMLURL != "" {
// Top-level feed without category
title := topOutline.Title
if title == "" {
title = topOutline.Text
}
feeds = append(feeds, ImportedFeed{
Title: title,
FeedURL: strings.TrimSpace(topOutline.XMLURL),
SiteURL: strings.TrimSpace(topOutline.HTMLURL),
CategoryTitle: "",
})
} else if len(topOutline.Outlines) > 0 {
// Category folder containing feeds
categoryTitle := topOutline.Title
if categoryTitle == "" {
categoryTitle = topOutline.Text
}
for _, child := range topOutline.Outlines {
if child.XMLURL != "" {
title := child.Title
if title == "" {
title = child.Text
}
feeds = append(feeds, ImportedFeed{
Title: title,
FeedURL: strings.TrimSpace(child.XMLURL),
SiteURL: strings.TrimSpace(child.HTMLURL),
CategoryTitle: strings.TrimSpace(categoryTitle),
})
}
}
}
}
return feeds, nil
}
func GenerateOPML(feeds []models.Feed, categories []models.Category) ([]byte, error) {
doc := OPML{
Version: "2.0",
Head: Head{
Title: "RSSPipe Subscriptions",
DateCreated: time.Now().UTC().Format(time.RFC1123),
DateModified: time.Now().UTC().Format(time.RFC1123),
},
Body: Body{
Outlines: make([]Outline, 0),
},
}
// Map category ID to slice of feeds
catFeedMap := make(map[string][]models.Feed)
var uncategorized []models.Feed
for _, f := range feeds {
if f.CategoryID != nil && *f.CategoryID != "" {
catFeedMap[*f.CategoryID] = append(catFeedMap[*f.CategoryID], f)
} else {
uncategorized = append(uncategorized, f)
}
}
// Add categorized feeds
for _, c := range categories {
feedList, exists := catFeedMap[c.ID]
if !exists || len(feedList) == 0 {
continue
}
catOutline := Outline{
Text: c.Title,
Title: c.Title,
Outlines: make([]Outline, 0, len(feedList)),
}
for _, f := range feedList {
catOutline.Outlines = append(catOutline.Outlines, Outline{
Text: f.Title,
Title: f.Title,
Type: "rss",
XMLURL: f.FeedURL,
HTMLURL: f.SiteURL,
})
}
doc.Body.Outlines = append(doc.Body.Outlines, catOutline)
}
// Add uncategorized feeds
for _, f := range uncategorized {
doc.Body.Outlines = append(doc.Body.Outlines, Outline{
Text: f.Title,
Title: f.Title,
Type: "rss",
XMLURL: f.FeedURL,
HTMLURL: f.SiteURL,
})
}
xmlHeader := []byte(xml.Header)
data, err := xml.MarshalIndent(doc, "", " ")
if err != nil {
return nil, err
}
return append(xmlHeader, data...), nil
}