package services import ( "context" "fmt" "strings" "git.pengzhan.dev/haopengzhan/rsspipe/internal/crawler" "git.pengzhan.dev/haopengzhan/rsspipe/internal/db" "git.pengzhan.dev/haopengzhan/rsspipe/internal/logger" "git.pengzhan.dev/haopengzhan/rsspipe/internal/models" "git.pengzhan.dev/haopengzhan/rsspipe/internal/opml" "git.pengzhan.dev/haopengzhan/rsspipe/internal/parser" "go.uber.org/zap" ) type FeedService struct { db *db.DB entryService *EntryService parser *parser.Parser crawler *crawler.Crawler log *zap.Logger } func NewFeedService(database *db.DB, entrySvc *EntryService, cr *crawler.Crawler) *FeedService { return &FeedService{ db: database, entryService: entrySvc, parser: parser.NewParser(), crawler: cr, log: logger.Named("feed-service"), } } func (s *FeedService) ListFeeds(ctx context.Context) ([]models.Feed, error) { return s.db.ListFeeds() } func (s *FeedService) GetFeed(ctx context.Context, id string) (*models.Feed, error) { return s.db.GetFeed(id) } func (s *FeedService) CreateFeed(ctx context.Context, req models.CreateFeedRequest) (*models.Feed, error) { feedURL := strings.TrimSpace(req.FeedURL) if feedURL == "" { return nil, fmt.Errorf("feed_url is required") } if existing, _ := s.db.GetFeedByURL(feedURL); existing != nil { return nil, fmt.Errorf("feed is already subscribed") } res, err := s.parser.FetchFeed(ctx, feedURL, "", "") if err != nil { return nil, fmt.Errorf("failed to fetch feed: %w", err) } title := res.Title if req.Title != nil && strings.TrimSpace(*req.Title) != "" { title = strings.TrimSpace(*req.Title) } iconURL := "" if req.IconURL != nil && strings.TrimSpace(*req.IconURL) != "" { iconURL = strings.TrimSpace(*req.IconURL) } feed, err := s.db.CreateFeed(title, feedURL, res.SiteURL, iconURL, req.CategoryID) if err != nil { return nil, err } if len(res.Entries) > 0 { _, _ = s.entryService.IngestEntries(ctx, feed.ID, res.Entries, feed.Title, feed.SiteURL) _ = s.db.UpdateFeedFetchStatus(feed.ID, res.ETag, res.LastModified, "") } return s.db.GetFeed(feed.ID) } func (s *FeedService) UpdateFeed(ctx context.Context, id string, req models.UpdateFeedRequest) (*models.Feed, error) { if err := s.db.UpdateFeed(id, req); err != nil { return nil, err } return s.db.GetFeed(id) } func (s *FeedService) DeleteFeed(ctx context.Context, id string) error { return s.db.DeleteFeed(id) } func (s *FeedService) RefreshFeed(ctx context.Context, id string) (*models.Feed, error) { if s.crawler != nil { if err := s.crawler.RefreshSingle(ctx, id); err != nil { return nil, err } } return s.db.GetFeed(id) } func (s *FeedService) RefreshAllFeeds(ctx context.Context) { if s.crawler != nil { go s.crawler.RefreshAll(context.Background()) } } // Category methods func (s *FeedService) ListCategories(ctx context.Context) ([]models.Category, error) { return s.db.ListCategories() } func (s *FeedService) CreateCategory(ctx context.Context, title string, parentID *string) (*models.Category, error) { return s.db.CreateCategory(strings.TrimSpace(title), parentID) } func (s *FeedService) DeleteCategory(ctx context.Context, id string) error { return s.db.DeleteCategory(id) } func (s *FeedService) GetStats(ctx context.Context) (*models.Stats, error) { return s.db.GetStats() } // OPML methods func (s *FeedService) ImportOPML(ctx context.Context, data []byte) (int, int, error) { importedFeeds, err := opml.ParseOPML(data) if err != nil { return 0, 0, fmt.Errorf("invalid OPML XML: %w", err) } categoryCache := make(map[string]string) cats, _ := s.db.ListCategories() for _, c := range cats { categoryCache[c.Title] = c.ID } createdCount := 0 for _, item := range importedFeeds { if item.FeedURL == "" { continue } if existing, _ := s.db.GetFeedByURL(item.FeedURL); existing != nil { continue } var catID *string if item.CategoryTitle != "" { if cid, ok := categoryCache[item.CategoryTitle]; ok { catID = &cid } else { newCat, err := s.db.CreateCategory(item.CategoryTitle, nil) if err == nil { categoryCache[item.CategoryTitle] = newCat.ID catID = &newCat.ID } } } title := item.Title if title == "" { title = item.FeedURL } _, err := s.db.CreateFeed(title, item.FeedURL, item.SiteURL, "", catID) if err == nil { createdCount++ } } if s.crawler != nil { go s.crawler.RefreshAll(context.Background()) } return createdCount, len(importedFeeds), nil } func (s *FeedService) ExportOPML(ctx context.Context) ([]byte, error) { feeds, err := s.db.ListFeeds() if err != nil { return nil, err } cats, err := s.db.ListCategories() if err != nil { return nil, err } return opml.GenerateOPML(feeds, cats) }