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 }