nandi/gleanpublic Fork 0
9a6ff22
Commits
Clone
git clone https://git.rickub.com/nandi/glean.git
git clone ssh://git@rickub.com/nandi/glean.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Improve fetcher performance and cleanup endpointsUnverified

Julien Robert committed 2026-04-22T17:06:58+02:00 Browse files
9a6ff22 parent: d1ec121
modified docs/specs.md +0 -1
@@ -844,7 +844,6 @@ The server renders HTML fragments that htmx swaps into the page. No JSON API nee
844844 | `/dashboard` | GET | Main dashboard: unread articles, recommendations sidebar |
845845 | `/feeds` | GET | Manage RSS subscriptions (OPML import for onboarding) |
846846 | `/feeds/list` | GET | Feed list fragment (htmx partial) |
847-| `/feeds/discover-url` | GET | Discover feed URL from a website |
848847 | `/feeds/opml/upload` | POST | Upload OPML file to bulk-import subscriptions |
849848 | `/feeds/opml/download` | GET | Export subscriptions as OPML (offboarding) |
850849 | `/feeds/add` | POST | Add a single feed URL |
@@ -844,7 +844,6 @@ The server renders HTML fragments that htmx swaps into the page. No JSON API nee
844 | `/dashboard` | GET | Main dashboard: unread articles, recommendations sidebar |844 | `/dashboard` | GET | Main dashboard: unread articles, recommendations sidebar |
845 | `/feeds` | GET | Manage RSS subscriptions (OPML import for onboarding) |845 | `/feeds` | GET | Manage RSS subscriptions (OPML import for onboarding) |
846 | `/feeds/list` | GET | Feed list fragment (htmx partial) |846 | `/feeds/list` | GET | Feed list fragment (htmx partial) |
847-| `/feeds/discover-url` | GET | Discover feed URL from a website |
848 | `/feeds/opml/upload` | POST | Upload OPML file to bulk-import subscriptions |847 | `/feeds/opml/upload` | POST | Upload OPML file to bulk-import subscriptions |
849 | `/feeds/opml/download` | GET | Export subscriptions as OPML (offboarding) |848 | `/feeds/opml/download` | GET | Export subscriptions as OPML (offboarding) |
850 | `/feeds/add` | POST | Add a single feed URL |849 | `/feeds/add` | POST | Add a single feed URL |
modified internal/cluster/scoring.go +1 -1
@@ -224,7 +224,7 @@ func (e *Engine) ComputeArticleRecommendationsOnDemand(ctx context.Context, user
224224 JOIN articles a ON a.feed_url = la.feed_url AND a.url = la.article_url
225225 LEFT JOIN feeds f ON f.feed_url = la.feed_url
226226 LEFT JOIN social_likes sl ON sl.feed_url = la.feed_url AND sl.article_url = la.article_url
227- ORDER BY score DESC
227+ ORDER BY score DESC, a.published DESC
228228 LIMIT ?
229229 `, userDID, userDID, userDID, userDID, userDID, userDID, w.WLike, w.WSocial, limit)
230230 if err != nil {
@@ -224,7 +224,7 @@ func (e *Engine) ComputeArticleRecommendationsOnDemand(ctx context.Context, user
224 JOIN articles a ON a.feed_url = la.feed_url AND a.url = la.article_url224 JOIN articles a ON a.feed_url = la.feed_url AND a.url = la.article_url
225 LEFT JOIN feeds f ON f.feed_url = la.feed_url225 LEFT JOIN feeds f ON f.feed_url = la.feed_url
226 LEFT JOIN social_likes sl ON sl.feed_url = la.feed_url AND sl.article_url = la.article_url226 LEFT JOIN social_likes sl ON sl.feed_url = la.feed_url AND sl.article_url = la.article_url
227- ORDER BY score DESC227+ ORDER BY score DESC, a.published DESC
228 LIMIT ?228 LIMIT ?
229 `, userDID, userDID, userDID, userDID, userDID, userDID, w.WLike, w.WSocial, limit)229 `, userDID, userDID, userDID, userDID, userDID, userDID, w.WLike, w.WSocial, limit)
230 if err != nil {230 if err != nil {
modified internal/db/article.go +52 -0
@@ -6,6 +6,8 @@ import (
66 "strings"
77 "time"
88 "unicode"
9+
10+ "pkg.rbrt.fr/glean/internal/feed"
911 )
1012
1113 type Article struct {
@@ -52,6 +54,56 @@ func (db *DB) UpsertArticle(ctx context.Context, article *Article) (int64, error
5254 return id, err
5355 }
5456
57+func (db *DB) UpsertArticlesBatch(ctx context.Context, articles []feed.Article) error {
58+ if len(articles) == 0 {
59+ return nil
60+ }
61+
62+ err := upsertArticlesBatch(ctx, db, articles)
63+ if err != nil {
64+ err = upsertArticlesBatch(ctx, db, articles)
65+ }
66+ return err
67+}
68+
69+func upsertArticlesBatch(ctx context.Context, db *DB, articles []feed.Article) error {
70+ tx, err := db.BeginTx(ctx, nil)
71+ if err != nil {
72+ return err
73+ }
74+ defer tx.Rollback()
75+
76+ stmt, err := tx.PrepareContext(ctx, `
77+ INSERT INTO articles (feed_url, guid, title, url, author, summary, content, published, updated)
78+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
79+ ON CONFLICT(feed_url, guid) DO NOTHING
80+ `)
81+ if err != nil {
82+ return err
83+ }
84+ defer stmt.Close()
85+
86+ for _, a := range articles {
87+ url := sql.NullString{String: a.URL, Valid: a.URL != ""}
88+ author := sql.NullString{String: a.Author, Valid: a.Author != ""}
89+ summary := sql.NullString{String: a.Summary, Valid: a.Summary != ""}
90+ content := sql.NullString{String: a.Content, Valid: a.Content != ""}
91+ var published, updated sql.NullTime
92+ if !a.Published.IsZero() {
93+ published = sql.NullTime{Time: a.Published, Valid: true}
94+ }
95+ if !a.Updated.IsZero() {
96+ updated = sql.NullTime{Time: a.Updated, Valid: true}
97+ }
98+
99+ if _, err := stmt.ExecContext(ctx, a.FeedURL, a.GUID, a.Title, url, author, summary, content, published, updated); err != nil {
100+ return err
101+ }
102+ }
103+
104+ return tx.Commit()
105+}
106+
55107 func (db *DB) GetArticle(ctx context.Context, id int64) (*Article, error) {
56108 a := &Article{}
57109 err := db.QueryRowContext(ctx, `
@@ -6,6 +6,8 @@ import (
6 "strings"6 "strings"
7 "time"7 "time"
8 "unicode"8 "unicode"
9+
10+ "pkg.rbrt.fr/glean/internal/feed"
9 )11 )
10 12
11 type Article struct {13 type Article struct {
@@ -52,6 +54,56 @@ func (db *DB) UpsertArticle(ctx context.Context, article *Article) (int64, error
52 return id, err54 return id, err
53 }55 }
54 56
57+func (db *DB) UpsertArticlesBatch(ctx context.Context, articles []feed.Article) error {
58+ if len(articles) == 0 {
59+ return nil
60+ }
61+
62+ err := upsertArticlesBatch(ctx, db, articles)
63+ if err != nil {
64+ err = upsertArticlesBatch(ctx, db, articles)
65+ }
66+ return err
67+}
68+
69+func upsertArticlesBatch(ctx context.Context, db *DB, articles []feed.Article) error {
70+ tx, err := db.BeginTx(ctx, nil)
71+ if err != nil {
72+ return err
73+ }
74+ defer tx.Rollback()
75+
76+ stmt, err := tx.PrepareContext(ctx, `
77+ INSERT INTO articles (feed_url, guid, title, url, author, summary, content, published, updated)
78+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
79+ ON CONFLICT(feed_url, guid) DO NOTHING
80+ `)
81+ if err != nil {
82+ return err
83+ }
84+ defer stmt.Close()
85+
86+ for _, a := range articles {
87+ url := sql.NullString{String: a.URL, Valid: a.URL != ""}
88+ author := sql.NullString{String: a.Author, Valid: a.Author != ""}
89+ summary := sql.NullString{String: a.Summary, Valid: a.Summary != ""}
90+ content := sql.NullString{String: a.Content, Valid: a.Content != ""}
91+ var published, updated sql.NullTime
92+ if !a.Published.IsZero() {
93+ published = sql.NullTime{Time: a.Published, Valid: true}
94+ }
95+ if !a.Updated.IsZero() {
96+ updated = sql.NullTime{Time: a.Updated, Valid: true}
97+ }
98+
99+ if _, err := stmt.ExecContext(ctx, a.FeedURL, a.GUID, a.Title, url, author, summary, content, published, updated); err != nil {
100+ return err
101+ }
102+ }
103+
104+ return tx.Commit()
105+}
106+
55 func (db *DB) GetArticle(ctx context.Context, id int64) (*Article, error) {107 func (db *DB) GetArticle(ctx context.Context, id int64) (*Article, error) {
56 a := &Article{}108 a := &Article{}
57 err := db.QueryRowContext(ctx, `109 err := db.QueryRowContext(ctx, `
modified internal/db/social.go +2 -2
@@ -255,7 +255,7 @@ func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since st
255255 UNION SELECT f.target_did FROM follows f WHERE f.user_did = ?
256256 )
257257 GROUP BY ar.id
258- ORDER BY like_count DESC, annotation_count DESC
258+ ORDER BY like_count DESC, annotation_count DESC, ar.published DESC
259259 LIMIT ? OFFSET ?
260260 `, since, userDID, since, userDID, userDID, userDID, userDID, userDID, limit, offset)
261261 if err != nil {
@@ -291,7 +291,7 @@ func (db *DB) ListTrendingArticles(ctx context.Context, userDID, since string, l
291291 LEFT JOIN likes ul ON ul.feed_url = l.feed_url AND ul.article_url = l.article_url AND ul.author_did = ?
292292 WHERE l.created_at >= ?
293293 GROUP BY ar.id
294- ORDER BY like_count DESC, annotation_count DESC
294+ ORDER BY like_count DESC, annotation_count DESC, ar.published DESC
295295 LIMIT ? OFFSET ?
296296 `, since, userDID, since, limit, offset)
297297 if err != nil {
@@ -255,7 +255,7 @@ func (db *DB) ListTrendingArticlesForUser(ctx context.Context, userDID, since st
255 UNION SELECT f.target_did FROM follows f WHERE f.user_did = ?255 UNION SELECT f.target_did FROM follows f WHERE f.user_did = ?
256 )256 )
257 GROUP BY ar.id257 GROUP BY ar.id
258- ORDER BY like_count DESC, annotation_count DESC258+ ORDER BY like_count DESC, annotation_count DESC, ar.published DESC
259 LIMIT ? OFFSET ?259 LIMIT ? OFFSET ?
260 `, since, userDID, since, userDID, userDID, userDID, userDID, userDID, limit, offset)260 `, since, userDID, since, userDID, userDID, userDID, userDID, userDID, limit, offset)
261 if err != nil {261 if err != nil {
@@ -291,7 +291,7 @@ func (db *DB) ListTrendingArticles(ctx context.Context, userDID, since string, l
291 LEFT JOIN likes ul ON ul.feed_url = l.feed_url AND ul.article_url = l.article_url AND ul.author_did = ?291 LEFT JOIN likes ul ON ul.feed_url = l.feed_url AND ul.article_url = l.article_url AND ul.author_did = ?
292 WHERE l.created_at >= ?292 WHERE l.created_at >= ?
293 GROUP BY ar.id293 GROUP BY ar.id
294- ORDER BY like_count DESC, annotation_count DESC294+ ORDER BY like_count DESC, annotation_count DESC, ar.published DESC
295 LIMIT ? OFFSET ?295 LIMIT ? OFFSET ?
296 `, since, userDID, since, limit, offset)296 `, since, userDID, since, limit, offset)
297 if err != nil {297 if err != nil {
modified internal/db/store.go +4 -0
@@ -56,6 +56,10 @@ func (a *FeedStoreAdapter) UpsertArticle(ctx context.Context, article *feed.Arti
5656 return a.db.UpsertArticle(ctx, dbArticle)
5757 }
5858
59+func (a *FeedStoreAdapter) UpsertArticlesBatch(ctx context.Context, articles []feed.Article) error {
60+ return a.db.UpsertArticlesBatch(ctx, articles)
61+}
62+
5963 func (a *FeedStoreAdapter) MarkFeedFetched(ctx context.Context, feedURL, etag, lastModified string) error {
6064 return a.db.MarkFeedFetched(ctx, feedURL, etag, lastModified)
6165 }
@@ -56,6 +56,10 @@ func (a *FeedStoreAdapter) UpsertArticle(ctx context.Context, article *feed.Arti
56 return a.db.UpsertArticle(ctx, dbArticle)56 return a.db.UpsertArticle(ctx, dbArticle)
57 }57 }
58 58
59+func (a *FeedStoreAdapter) UpsertArticlesBatch(ctx context.Context, articles []feed.Article) error {
60+ return a.db.UpsertArticlesBatch(ctx, articles)
61+}
62+
59 func (a *FeedStoreAdapter) MarkFeedFetched(ctx context.Context, feedURL, etag, lastModified string) error {63 func (a *FeedStoreAdapter) MarkFeedFetched(ctx context.Context, feedURL, etag, lastModified string) error {
60 return a.db.MarkFeedFetched(ctx, feedURL, etag, lastModified)64 return a.db.MarkFeedFetched(ctx, feedURL, etag, lastModified)
61 }65 }
modified internal/feed/discover.go +34 -13
@@ -116,21 +116,42 @@ func findFavicon(ctx context.Context, base *url.URL, links []string) string {
116116 origin.RawQuery = ""
117117 origin.Fragment = ""
118118
119+ type result struct {
120+ url string
121+ found bool
122+ }
123+ found := make(chan result, 1)
124+ ctx, cancel := context.WithCancel(ctx)
125+ defer cancel()
126+
119127 for _, path := range faviconPaths {
120- u, _ := url.Parse(path)
121- resolved := origin.ResolveReference(u)
122- req, err := http.NewRequestWithContext(ctx, http.MethodGet, resolved.String(), nil)
123- if err != nil {
124- continue
125- }
126- resp, err := discoverClient.Do(req)
127- if err != nil {
128- continue
129- }
130- resp.Body.Close()
131- if resp.StatusCode == http.StatusOK && imageContentTypes.matches(resp.Header.Get("Content-Type")) {
132- return cleanFavicon(resolved.String())
128+ go func(path string) {
129+ u, _ := url.Parse(path)
130+ resolved := origin.ResolveReference(u)
131+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, resolved.String(), nil)
132+ if err != nil {
133+ return
134+ }
135+ resp, err := discoverClient.Do(req)
136+ if err != nil {
137+ return
138+ }
139+ resp.Body.Close()
140+ if resp.StatusCode == http.StatusOK && imageContentTypes.matches(resp.Header.Get("Content-Type")) {
141+ select {
142+ case found <- result{url: cleanFavicon(resolved.String()), found: true}:
143+ default:
144+ }
145+ }
146+ }(path)
147+ }
148+
149+ select {
150+ case r := <-found:
151+ if r.found {
152+ return r.url
133153 }
154+ case <-ctx.Done():
134155 }
135156 return ""
136157 }
@@ -116,21 +116,42 @@ func findFavicon(ctx context.Context, base *url.URL, links []string) string {
116 origin.RawQuery = ""116 origin.RawQuery = ""
117 origin.Fragment = ""117 origin.Fragment = ""
118 118
119+ type result struct {
120+ url string
121+ found bool
122+ }
123+ found := make(chan result, 1)
124+ ctx, cancel := context.WithCancel(ctx)
125+ defer cancel()
126+
119 for _, path := range faviconPaths {127 for _, path := range faviconPaths {
120- u, _ := url.Parse(path)128+ go func(path string) {
121- resolved := origin.ResolveReference(u)129+ u, _ := url.Parse(path)
122- req, err := http.NewRequestWithContext(ctx, http.MethodGet, resolved.String(), nil)130+ resolved := origin.ResolveReference(u)
123- if err != nil {131+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, resolved.String(), nil)
124- continue132+ if err != nil {
125- }133+ return
126- resp, err := discoverClient.Do(req)134+ }
127- if err != nil {135+ resp, err := discoverClient.Do(req)
128- continue136+ if err != nil {
129- }137+ return
130- resp.Body.Close()138+ }
131- if resp.StatusCode == http.StatusOK && imageContentTypes.matches(resp.Header.Get("Content-Type")) {139+ resp.Body.Close()
132- return cleanFavicon(resolved.String())140+ if resp.StatusCode == http.StatusOK && imageContentTypes.matches(resp.Header.Get("Content-Type")) {
141+ select {
142+ case found <- result{url: cleanFavicon(resolved.String()), found: true}:
143+ default:
144+ }
145+ }
146+ }(path)
147+ }
148+
149+ select {
150+ case r := <-found:
151+ if r.found {
152+ return r.url
133 }153 }
154+ case <-ctx.Done():
134 }155 }
135 return ""156 return ""
136 }157 }
modified internal/feed/fetcher.go +9 -8
@@ -70,6 +70,7 @@ func (f *Fetcher) Fetch(ctx context.Context, feedURL, etag, lastModified string)
7070 type FeedStore interface {
7171 GetFeedsToFetch(ctx context.Context, olderThan time.Duration, limit int) ([]*Feed, error)
7272 UpsertArticle(ctx context.Context, article *Article) (int64, error)
73+ UpsertArticlesBatch(ctx context.Context, articles []Article) error
7374 MarkFeedFetched(ctx context.Context, feedURL, etag, lastModified string) error
7475 MarkFeedFetchError(ctx context.Context, feedURL, lastError string) error
7576 UpdateFeedFavicon(ctx context.Context, feedURL, faviconURL string) error
@@ -120,7 +121,7 @@ func (s *Scheduler) fetchAll(ctx context.Context) {
120121 return
121122 }
122123
123- sem := make(chan struct{}, 3)
124+ sem := make(chan struct{}, 10)
124125 var wg sync.WaitGroup
125126 for _, f := range feeds {
126127 wg.Add(1)
@@ -169,13 +170,13 @@ func (s *Scheduler) FetchFeed(ctx context.Context, feed *Feed) {
169170
170171 metrics.FeedsFetched.WithLabelValues("success").Inc()
171172
172- for i := range result.Articles {
173- result.Articles[i].FeedURL = feed.URL
174- if _, upsertErr := s.store.UpsertArticle(ctx, &result.Articles[i]); upsertErr != nil {
175- s.logger.Error("failed to upsert article", "error", upsertErr, "url", result.Articles[i].URL)
176- } else {
177- metrics.ArticlesUpserted.Inc()
178- }
173+ for _, article := range result.Articles {
174+ article.FeedURL = feed.URL
175+ }
176+ if err := s.store.UpsertArticlesBatch(ctx, result.Articles); err != nil {
177+ s.logger.Error("failed to upsert articles", "error", err, "feed", feed.URL)
178+ } else {
179+ metrics.ArticlesUpserted.Add(float64(len(result.Articles)))
179180 }
180181
181182 if err := s.store.MarkFeedFetched(ctx, feed.URL, newEtag, newLastModified); err != nil {
@@ -70,6 +70,7 @@ func (f *Fetcher) Fetch(ctx context.Context, feedURL, etag, lastModified string)
70 type FeedStore interface {70 type FeedStore interface {
71 GetFeedsToFetch(ctx context.Context, olderThan time.Duration, limit int) ([]*Feed, error)71 GetFeedsToFetch(ctx context.Context, olderThan time.Duration, limit int) ([]*Feed, error)
72 UpsertArticle(ctx context.Context, article *Article) (int64, error)72 UpsertArticle(ctx context.Context, article *Article) (int64, error)
73+ UpsertArticlesBatch(ctx context.Context, articles []Article) error
73 MarkFeedFetched(ctx context.Context, feedURL, etag, lastModified string) error74 MarkFeedFetched(ctx context.Context, feedURL, etag, lastModified string) error
74 MarkFeedFetchError(ctx context.Context, feedURL, lastError string) error75 MarkFeedFetchError(ctx context.Context, feedURL, lastError string) error
75 UpdateFeedFavicon(ctx context.Context, feedURL, faviconURL string) error76 UpdateFeedFavicon(ctx context.Context, feedURL, faviconURL string) error
@@ -120,7 +121,7 @@ func (s *Scheduler) fetchAll(ctx context.Context) {
120 return121 return
121 }122 }
122 123
123- sem := make(chan struct{}, 3)124+ sem := make(chan struct{}, 10)
124 var wg sync.WaitGroup125 var wg sync.WaitGroup
125 for _, f := range feeds {126 for _, f := range feeds {
126 wg.Add(1)127 wg.Add(1)
@@ -169,13 +170,13 @@ func (s *Scheduler) FetchFeed(ctx context.Context, feed *Feed) {
169 170
170 metrics.FeedsFetched.WithLabelValues("success").Inc()171 metrics.FeedsFetched.WithLabelValues("success").Inc()
171 172
172- for i := range result.Articles {173+ for _, article := range result.Articles {
173- result.Articles[i].FeedURL = feed.URL174+ article.FeedURL = feed.URL
174- if _, upsertErr := s.store.UpsertArticle(ctx, &result.Articles[i]); upsertErr != nil {175+ }
175- s.logger.Error("failed to upsert article", "error", upsertErr, "url", result.Articles[i].URL)176+ if err := s.store.UpsertArticlesBatch(ctx, result.Articles); err != nil {
176- } else {177+ s.logger.Error("failed to upsert articles", "error", err, "feed", feed.URL)
177- metrics.ArticlesUpserted.Inc()178+ } else {
178- }179+ metrics.ArticlesUpserted.Add(float64(len(result.Articles)))
179 }180 }
180 181
181 if err := s.store.MarkFeedFetched(ctx, feed.URL, newEtag, newLastModified); err != nil {182 if err := s.store.MarkFeedFetched(ctx, feed.URL, newEtag, newLastModified); err != nil {
modified internal/server/feeds_handler.go +0 -26
@@ -3,7 +3,6 @@ package server
33 import (
44 "context"
55 "database/sql"
6- "encoding/json"
76 "errors"
87 "net/http"
98 "time"
@@ -374,31 +373,6 @@ func (s *Server) handleRetryFeed(w http.ResponseWriter, r *http.Request) {
374373 })
375374 }
376375
377-func (s *Server) handleDiscoverFeedURL(w http.ResponseWriter, r *http.Request) {
378- siteURL := r.URL.Query().Get("url")
379- if siteURL == "" {
380- http.Error(w, "url required", http.StatusBadRequest)
381- return
382- }
383-
384- result, err := feed.Discover(r.Context(), siteURL)
385- if err != nil {
386- s.logger.Error("feed discovery failed", "error", err, "url", siteURL)
387- http.Error(w, err.Error(), http.StatusInternalServerError)
388- return
389- }
390-
391- w.Header().Set("Content-Type", "application/json")
392- type discoveryResponse struct {
393- FeedURLs []string `json:"feed_urls"`
394- Favicon string `json:"favicon"`
395- }
396- json.NewEncoder(w).Encode(discoveryResponse{
397- FeedURLs: result.FeedURLs,
398- Favicon: result.Favicon,
399- })
400-}
401-
402376 func (s *Server) handleDismissFeedRecommendation(w http.ResponseWriter, r *http.Request) {
403377 user := currentUser(r)
404378 feedURL := r.FormValue("feed_url")
@@ -3,7 +3,6 @@ package server
3 import (3 import (
4 "context"4 "context"
5 "database/sql"5 "database/sql"
6- "encoding/json"
7 "errors"6 "errors"
8 "net/http"7 "net/http"
9 "time"8 "time"
@@ -374,31 +373,6 @@ func (s *Server) handleRetryFeed(w http.ResponseWriter, r *http.Request) {
374 })373 })
375 }374 }
376 375
377-func (s *Server) handleDiscoverFeedURL(w http.ResponseWriter, r *http.Request) {
378- siteURL := r.URL.Query().Get("url")
379- if siteURL == "" {
380- http.Error(w, "url required", http.StatusBadRequest)
381- return
382- }
383-
384- result, err := feed.Discover(r.Context(), siteURL)
385- if err != nil {
386- s.logger.Error("feed discovery failed", "error", err, "url", siteURL)
387- http.Error(w, err.Error(), http.StatusInternalServerError)
388- return
389- }
390-
391- w.Header().Set("Content-Type", "application/json")
392- type discoveryResponse struct {
393- FeedURLs []string `json:"feed_urls"`
394- Favicon string `json:"favicon"`
395- }
396- json.NewEncoder(w).Encode(discoveryResponse{
397- FeedURLs: result.FeedURLs,
398- Favicon: result.Favicon,
399- })
400-}
401-
402 func (s *Server) handleDismissFeedRecommendation(w http.ResponseWriter, r *http.Request) {376 func (s *Server) handleDismissFeedRecommendation(w http.ResponseWriter, r *http.Request) {
403 user := currentUser(r)377 user := currentUser(r)
404 feedURL := r.FormValue("feed_url")378 feedURL := r.FormValue("feed_url")
modified internal/server/server.go +0 -1
@@ -161,7 +161,6 @@ func (s *Server) setupRoutes() {
161161 r.Post("/refresh", s.handleRefreshFeeds)
162162 r.Post("/retry", s.handleRetryFeed)
163163 r.Get("/list", s.handleFeedList)
164- r.Get("/discover-url", s.handleDiscoverFeedURL)
165164 r.Post("/clear", s.handleClearAllSubscriptions)
166165 r.Post("/dismiss", s.handleDismissFeedRecommendation)
167166 })
@@ -161,7 +161,6 @@ func (s *Server) setupRoutes() {
161 r.Post("/refresh", s.handleRefreshFeeds)161 r.Post("/refresh", s.handleRefreshFeeds)
162 r.Post("/retry", s.handleRetryFeed)162 r.Post("/retry", s.handleRetryFeed)
163 r.Get("/list", s.handleFeedList)163 r.Get("/list", s.handleFeedList)
164- r.Get("/discover-url", s.handleDiscoverFeedURL)
165 r.Post("/clear", s.handleClearAllSubscriptions)164 r.Post("/clear", s.handleClearAllSubscriptions)
166 r.Post("/dismiss", s.handleDismissFeedRecommendation)165 r.Post("/dismiss", s.handleDismissFeedRecommendation)
167 })166 })