aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-01 10:05:13 +0900
committernsfisis <nsfisis@gmail.com>2026-02-01 10:05:13 +0900
commit1d78371e13c19488d472257ab614e0be17bfa85e (patch)
tree6c14cf068faac9a9a411d1cf6c436ae23ba235c2
parent65d6ebca318fecb48b32c0ecdba3ae01304b55de (diff)
downloadfeedaka-1d78371e13c19488d472257ab614e0be17bfa85e.tar.gz
feedaka-1d78371e13c19488d472257ab614e0be17bfa85e.tar.zst
feedaka-1d78371e13c19488d472257ab614e0be17bfa85e.zip
refactor: extract feed fetch and sync logic into shared feed package
Consolidate duplicated feed fetching and article syncing code from AddFeed resolver and fetchOneFeed into reusable feed.Fetch and feed.Sync functions. This unifies behavior (10s timeout, article updates) across both call sites. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
-rw-r--r--backend/cmd/serve.go62
-rw-r--r--backend/feed/feed.go75
-rw-r--r--backend/graphql/resolver/schema.resolvers.go33
3 files changed, 84 insertions, 86 deletions
diff --git a/backend/cmd/serve.go b/backend/cmd/serve.go
index 30d0702..e2d2df5 100644
--- a/backend/cmd/serve.go
+++ b/backend/cmd/serve.go
@@ -4,7 +4,6 @@ import (
"context"
"database/sql"
"embed"
- "fmt"
"log"
"net/http"
"os"
@@ -20,78 +19,23 @@ import (
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
- "github.com/mmcdole/gofeed"
"github.com/vektah/gqlparser/v2/ast"
"undef.ninja/x/feedaka/auth"
"undef.ninja/x/feedaka/config"
"undef.ninja/x/feedaka/db"
+ "undef.ninja/x/feedaka/feed"
"undef.ninja/x/feedaka/graphql"
"undef.ninja/x/feedaka/graphql/resolver"
)
func fetchOneFeed(feedID int64, url string, ctx context.Context, queries *db.Queries) error {
log.Printf("Fetching %s...\n", url)
- fp := gofeed.NewParser()
- ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
- defer cancel()
- feed, err := fp.ParseURLWithContext(url, ctx)
- if err != nil {
- return fmt.Errorf("Failed to fetch %s: %v\n", url, err)
- }
- err = queries.UpdateFeedMetadata(ctx, db.UpdateFeedMetadataParams{
- Title: feed.Title,
- FetchedAt: time.Now().UTC().Format(time.RFC3339),
- ID: feedID,
- })
- if err != nil {
- return err
- }
- // Get GUIDs for this feed (for updating existing articles)
- guids, err := queries.GetArticleGUIDsByFeed(ctx, feedID)
+ f, err := feed.Fetch(ctx, url)
if err != nil {
return err
}
- existingFeedGUIDs := make(map[string]bool)
- for _, guid := range guids {
- existingFeedGUIDs[guid] = true
- }
- for _, item := range feed.Items {
- if existingFeedGUIDs[item.GUID] {
- // Article exists in this feed, update it
- err := queries.UpdateArticle(ctx, db.UpdateArticleParams{
- Title: item.Title,
- Url: item.Link,
- FeedID: feedID,
- Guid: item.GUID,
- })
- if err != nil {
- return err
- }
- } else {
- // Check if article with same GUID exists globally (in any feed)
- exists, err := queries.CheckArticleExistsByGUID(ctx, item.GUID)
- if err != nil {
- return err
- }
- if exists == 1 {
- // Article already exists in another feed, skip
- continue
- }
- // Create new article
- _, err = queries.CreateArticle(ctx, db.CreateArticleParams{
- FeedID: feedID,
- Guid: item.GUID,
- Title: item.Title,
- Url: item.Link,
- IsRead: 0,
- })
- if err != nil {
- return err
- }
- }
- }
- return nil
+ return feed.Sync(ctx, queries, feedID, f)
}
func listFeedsToBeFetched(ctx context.Context, queries *db.Queries) (map[int64]string, error) {
diff --git a/backend/feed/feed.go b/backend/feed/feed.go
new file mode 100644
index 0000000..4349d1e
--- /dev/null
+++ b/backend/feed/feed.go
@@ -0,0 +1,75 @@
+package feed
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/mmcdole/gofeed"
+
+ "undef.ninja/x/feedaka/db"
+)
+
+func Fetch(ctx context.Context, url string) (*gofeed.Feed, error) {
+ fp := gofeed.NewParser()
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+ feed, err := fp.ParseURLWithContext(url, ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to fetch %s: %w", url, err)
+ }
+ return feed, nil
+}
+
+func Sync(ctx context.Context, queries *db.Queries, feedID int64, f *gofeed.Feed) error {
+ err := queries.UpdateFeedMetadata(ctx, db.UpdateFeedMetadataParams{
+ Title: f.Title,
+ FetchedAt: time.Now().UTC().Format(time.RFC3339),
+ ID: feedID,
+ })
+ if err != nil {
+ return err
+ }
+
+ guids, err := queries.GetArticleGUIDsByFeed(ctx, feedID)
+ if err != nil {
+ return err
+ }
+ existingFeedGUIDs := make(map[string]bool, len(guids))
+ for _, guid := range guids {
+ existingFeedGUIDs[guid] = true
+ }
+
+ for _, item := range f.Items {
+ if existingFeedGUIDs[item.GUID] {
+ err := queries.UpdateArticle(ctx, db.UpdateArticleParams{
+ Title: item.Title,
+ Url: item.Link,
+ FeedID: feedID,
+ Guid: item.GUID,
+ })
+ if err != nil {
+ return err
+ }
+ } else {
+ exists, err := queries.CheckArticleExistsByGUID(ctx, item.GUID)
+ if err != nil {
+ return err
+ }
+ if exists == 1 {
+ continue
+ }
+ _, err = queries.CreateArticle(ctx, db.CreateArticleParams{
+ FeedID: feedID,
+ Guid: item.GUID,
+ Title: item.Title,
+ Url: item.Link,
+ IsRead: 0,
+ })
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
diff --git a/backend/graphql/resolver/schema.resolvers.go b/backend/graphql/resolver/schema.resolvers.go
index c3f6f0a..10f892f 100644
--- a/backend/graphql/resolver/schema.resolvers.go
+++ b/backend/graphql/resolver/schema.resolvers.go
@@ -11,9 +11,9 @@ import (
"strconv"
"time"
- "github.com/mmcdole/gofeed"
"undef.ninja/x/feedaka/auth"
"undef.ninja/x/feedaka/db"
+ "undef.ninja/x/feedaka/feed"
gql "undef.ninja/x/feedaka/graphql"
"undef.ninja/x/feedaka/graphql/model"
)
@@ -26,8 +26,7 @@ func (r *mutationResolver) AddFeed(ctx context.Context, url string) (*model.Feed
}
// Fetch the feed to get its title
- fp := gofeed.NewParser()
- feed, err := fp.ParseURL(url)
+ f, err := feed.Fetch(ctx, url)
if err != nil {
return nil, fmt.Errorf("failed to parse feed: %w", err)
}
@@ -35,7 +34,7 @@ func (r *mutationResolver) AddFeed(ctx context.Context, url string) (*model.Feed
// Insert the feed into the database
dbFeed, err := r.Queries.CreateFeed(ctx, db.CreateFeedParams{
Url: url,
- Title: feed.Title,
+ Title: f.Title,
FetchedAt: time.Now().UTC().Format(time.RFC3339),
UserID: userID,
})
@@ -43,29 +42,9 @@ func (r *mutationResolver) AddFeed(ctx context.Context, url string) (*model.Feed
return nil, fmt.Errorf("failed to insert feed: %w", err)
}
- // Insert articles from the feed (skip duplicates by guid)
- for _, item := range feed.Items {
- // Check if article with same GUID already exists globally
- exists, err := r.Queries.CheckArticleExistsByGUID(ctx, item.GUID)
- if err != nil {
- fmt.Printf("Failed to check article existence: %v\n", err)
- continue
- }
- if exists == 1 {
- // Article already exists, skip
- continue
- }
- _, err = r.Queries.CreateArticle(ctx, db.CreateArticleParams{
- FeedID: dbFeed.ID,
- Guid: item.GUID,
- Title: item.Title,
- Url: item.Link,
- IsRead: 0,
- })
- if err != nil {
- // Log but don't fail on individual article errors
- fmt.Printf("Failed to insert article: %v\n", err)
- }
+ // Sync articles from the feed
+ if err := feed.Sync(ctx, r.Queries, dbFeed.ID, f); err != nil {
+ return nil, fmt.Errorf("failed to sync articles: %w", err)
}
return &model.Feed{