mirror of
https://github.com/R0m1k3/FlowReader.git
synced 2026-10-11 17:28:05 +02:00
feat(story-2.2): add feed parser with gofeed and article repository
This commit is contained in:
1 parent
1249e1bd42
commit
12f0c6879f
5 files changed
+646
-2
No files matched your search
@@ -0,0 +1,136 @@
|
||||
// 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
|
||||
}
|
||||
Reference in new issue
Block a user