package feeds import ( "encoding/xml" "fmt" "io" "log" "strings" "time" "github.com/go-co-op/gocron" "github.com/go-shiori/go-readability" "github.com/mmcdole/gofeed" "gorm.io/gorm" ) // Service handles feed operations type Service struct { db *gorm.DB parser *gofeed.Parser cron *gocron.Scheduler } // NewService creates a new feed service func NewService(db *gorm.DB) *Service { s := &Service{ db: db, parser: gofeed.NewParser(), cron: gocron.NewScheduler(time.UTC), } // Start the scheduler _, err := s.cron.Every(1).Hour().Do(s.RefreshAllFeeds) if err != nil { log.Printf("Error scheduling feed refresh: %v", err) } s.cron.StartAsync() return s } // AddFeed adds a new feed func (s *Service) AddFeed(url string) (*Feed, error) { // Check if feed already exists var existingFeed Feed if result := s.db.Where("url = ?", url).First(&existingFeed); result.Error == nil { return &existingFeed, nil } // Fetch and parse the feed feed, err := s.parser.ParseURL(url) if err != nil { return nil, fmt.Errorf("failed to parse feed: %w", err) } // Create new feed newFeed := Feed{ Title: feed.Title, URL: url, Description: feed.Description, SiteURL: feed.Link, ImageURL: s.extractFeedImage(feed), LastFetched: time.Now(), CreatedAt: time.Now(), UpdatedAt: time.Now(), } // Save feed to database if err := s.db.Create(&newFeed).Error; err != nil { return nil, fmt.Errorf("failed to save feed: %w", err) } // Process feed entries s.processFeedEntries(&newFeed, feed) return &newFeed, nil } // ImportOPML imports feeds from an OPML file func (s *Service) ImportOPML(opmlContent io.Reader) ([]Feed, error) { // Read the content into a buffer so we can inspect it if there's an error content, err := io.ReadAll(opmlContent) if err != nil { return nil, fmt.Errorf("failed to read OPML content: %w", err) } // Check if the content looks like XML trimmedContent := strings.TrimSpace(string(content)) if !strings.HasPrefix(trimmedContent, "