158 lines
3.7 KiB
Go
158 lines
3.7 KiB
Go
package opml
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.pengzhan.dev/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
|
|
}
|