mirror of
https://github.com/R0m1k3/FlowReader.git
synced 2026-10-12 01:38:16 +02:00
137 lines
2.8 KiB
Go
137 lines
2.8 KiB
Go
// Package parser provides RSS/Atom feed parsing functionality.
|
|
package parser
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/michael/flowreader/internal/domain"
|
|
"github.com/mmcdole/gofeed"
|
|
)
|
|
|
|
// FeedParser handles RSS/Atom feed parsing.
|
|
type FeedParser struct {
|
|
client *http.Client
|
|
parser *gofeed.Parser
|
|
}
|
|
|
|
// NewFeedParser creates a new feed parser.
|
|
func NewFeedParser() *FeedParser {
|
|
client := &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
|
|
return &FeedParser{
|
|
client: client,
|
|
parser: gofeed.NewParser(),
|
|
}
|
|
}
|
|
|
|
// ParsedFeed contains the parsed feed data.
|
|
type ParsedFeed struct {
|
|
Title string
|
|
Description string
|
|
SiteURL string
|
|
ImageURL string
|
|
Articles []*domain.Article
|
|
}
|
|
|
|
// Parse fetches and parses a feed URL.
|
|
func (p *FeedParser) Parse(ctx context.Context, feedURL string, feedID uuid.UUID) (*ParsedFeed, error) {
|
|
// Create request with context
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, feedURL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating request: %w", err)
|
|
}
|
|
req.Header.Set("User-Agent", "FlowReader/1.0 (RSS Reader)")
|
|
|
|
// Fetch the feed
|
|
resp, err := p.client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fetching feed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
|
}
|
|
|
|
// Parse the feed
|
|
feed, err := p.parser.Parse(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing feed: %w", err)
|
|
}
|
|
|
|
// Extract metadata
|
|
parsed := &ParsedFeed{
|
|
Title: feed.Title,
|
|
Description: feed.Description,
|
|
}
|
|
|
|
if feed.Link != "" {
|
|
parsed.SiteURL = feed.Link
|
|
}
|
|
|
|
if feed.Image != nil && feed.Image.URL != "" {
|
|
parsed.ImageURL = feed.Image.URL
|
|
}
|
|
|
|
// Convert items to articles
|
|
for _, item := range feed.Items {
|
|
article := &domain.Article{
|
|
ID: uuid.New(),
|
|
FeedID: feedID,
|
|
GUID: getGUID(item),
|
|
Title: item.Title,
|
|
}
|
|
|
|
if item.Link != "" {
|
|
article.URL = item.Link
|
|
}
|
|
|
|
if item.Content != "" {
|
|
article.Content = item.Content
|
|
}
|
|
|
|
if item.Description != "" {
|
|
article.Summary = item.Description
|
|
}
|
|
|
|
if item.Author != nil {
|
|
article.Author = item.Author.Name
|
|
} else if len(item.Authors) > 0 {
|
|
article.Author = item.Authors[0].Name
|
|
}
|
|
|
|
if item.Image != nil && item.Image.URL != "" {
|
|
article.ImageURL = item.Image.URL
|
|
}
|
|
|
|
if item.PublishedParsed != nil {
|
|
article.PublishedAt = item.PublishedParsed
|
|
} else if item.UpdatedParsed != nil {
|
|
article.PublishedAt = item.UpdatedParsed
|
|
}
|
|
|
|
article.CreatedAt = time.Now()
|
|
|
|
parsed.Articles = append(parsed.Articles, article)
|
|
}
|
|
|
|
return parsed, nil
|
|
}
|
|
|
|
// getGUID returns a unique identifier for the feed item.
|
|
func getGUID(item *gofeed.Item) string {
|
|
if item.GUID != "" {
|
|
return item.GUID
|
|
}
|
|
if item.Link != "" {
|
|
return item.Link
|
|
}
|
|
return item.Title // Last resort fallback
|
|
}
|