feat: scaffold initial RSS reader project with Go backend and React frontend
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user