A social RSS reader built on the AT Protocol. glean.at
glean atproto atmosphere rss feed social app
14
fork

Configure Feed

Select the types of activity you want to include in your feed.

Exclude feeds from retry loop after repeated errors

+74 -25
+1 -1
internal/db/feed.go
··· 77 77 last_fetched_at, last_error, subscriber_count, etag, last_modified, 78 78 fetch_interval_minutes, next_fetch_at, consecutive_empty_fetches, error_count, favicon_url 79 79 FROM feeds 80 - WHERE subscriber_count > 0 AND (last_fetched_at IS NULL OR last_fetched_at <= ?) 80 + WHERE subscriber_count > 0 AND error_count < 25 AND (last_fetched_at IS NULL OR last_fetched_at <= ?) 81 81 ORDER BY last_fetched_at ASC NULLS FIRST 82 82 LIMIT ? 83 83 `, cutoff, limit)
+37
internal/server/feeds_handler.go
··· 322 322 }) 323 323 } 324 324 325 + func (s *Server) handleRetryFeed(w http.ResponseWriter, r *http.Request) { 326 + feedURL := r.FormValue("url") 327 + if feedURL == "" { 328 + http.Error(w, "url required", http.StatusBadRequest) 329 + return 330 + } 331 + 332 + f, err := s.db.GetFeed(r.Context(), feedURL) 333 + if err != nil { 334 + http.Error(w, "feed not found", http.StatusNotFound) 335 + return 336 + } 337 + 338 + ff := &feed.Feed{ 339 + URL: f.FeedURL, 340 + Title: f.Title.String, 341 + SiteURL: f.SiteURL.String, 342 + Description: f.Description.String, 343 + Type: f.FeedType.String, 344 + ETag: f.Etag.String, 345 + LastModified: f.LastModified.String, 346 + } 347 + s.scheduler.FetchFeed(r.Context(), ff) 348 + 349 + user := currentUser(r) 350 + deadFeeds, _ := s.db.ListDeadFeeds(r.Context(), user.DID, 7) 351 + if len(deadFeeds) == 0 { 352 + w.Header().Set("Content-Type", "text/html") 353 + w.Write([]byte("")) 354 + return 355 + } 356 + 357 + s.render(w, r, "dead-feeds.html", map[string]any{ 358 + "DeadFeeds": deadFeeds, 359 + }) 360 + } 361 + 325 362 func (s *Server) handleDiscoverFeedURL(w http.ResponseWriter, r *http.Request) { 326 363 siteURL := r.URL.Query().Get("url") 327 364 if siteURL == "" {
+1
internal/server/server.go
··· 158 158 r.Post("/opml/upload", s.handleOPMLUpload) 159 159 r.Get("/opml/download", s.handleOPMLDownload) 160 160 r.Post("/refresh", s.handleRefreshFeeds) 161 + r.Post("/retry", s.handleRetryFeed) 161 162 r.Get("/list", s.handleFeedList) 162 163 r.Get("/discover-url", s.handleDiscoverFeedURL) 163 164 r.Post("/clear", s.handleClearAllSubscriptions)
+1 -24
internal/tmpl/feeds.html
··· 12 12 </div> 13 13 <p class="text-sm text-spot-secondary mb-6">Manage your RSS and Atom subscriptions.</p> 14 14 15 - {{if .DeadFeeds}} 16 - <div id="dead-feeds" class="bg-spot-red/10 border border-spot-red/30 rounded-lg p-4 mb-6"> 17 - <h3 class="text-sm font-bold text-spot-red mb-2">Feeds with errors ({{len .DeadFeeds}})</h3> 18 - <div class="space-y-2"> 19 - {{range .DeadFeeds}} 20 - <div class="dead-feed-item flex items-start justify-between gap-3 text-sm flex-wrap"> 21 - <div class="min-w-0 flex-1"> 22 - <span class="text-spot-text truncate block">{{if .Title.Valid}}{{.Title.String}}{{else}}{{.FeedURL}}{{end}}</span> 23 - <div class="flex items-center gap-2 mt-1 flex-wrap"> 24 - <span class="text-spot-red text-xs">{{.ErrorCount}} errors</span> 25 - {{if .LastError.Valid}}<span class="text-spot-secondary text-xs truncate max-w-48" title="{{.LastError.String}}">{{.LastError.String}}</span>{{end}} 26 - </div> 27 - </div> 28 - <form hx-delete="/feeds/remove" hx-target="closest .dead-feed-item" hx-swap="outerHTML swap:0.3s" 29 - hx-confirm="Unsubscribe from this broken feed?" class="inline shrink-0"> 30 - {{csrfInput $.CSRFToken}} 31 - <input type="hidden" name="url" value="{{.FeedURL}}"> 32 - <button type="submit" class="text-xs text-spot-secondary hover:text-spot-red transition font-bold uppercase">Unsubscribe</button> 33 - </form> 34 - </div> 35 - {{end}} 36 - </div> 37 - </div> 38 - {{end}} 15 + {{template "dead-feeds.html" (dict "DeadFeeds" .DeadFeeds "CSRFToken" .CSRFToken)}} 39 16 40 17 {{if .FeedRecommendations}} 41 18 <div class="mb-6">
+34
internal/tmpl/partials/dead-feeds.html
··· 1 + {{define "dead-feeds.html"}} 2 + {{if .DeadFeeds}} 3 + <div id="dead-feeds" class="bg-spot-red/10 border border-spot-red/30 rounded-lg p-4 mb-6"> 4 + <h3 class="text-sm font-bold text-spot-red mb-2">Feeds with errors ({{len .DeadFeeds}})</h3> 5 + <div class="space-y-2"> 6 + {{range .DeadFeeds}} 7 + <div class="dead-feed-item flex items-start justify-between gap-3 text-sm flex-wrap"> 8 + <div class="min-w-0 flex-1"> 9 + <span class="text-spot-text truncate block">{{if .Title.Valid}}{{.Title.String}}{{else}}{{.FeedURL}}{{end}}</span> 10 + <div class="flex items-center gap-2 mt-1 flex-wrap"> 11 + <span class="text-spot-red text-xs">{{.ErrorCount}} errors</span> 12 + {{if .LastError.Valid}}<span class="text-spot-secondary text-xs truncate max-w-48" title="{{.LastError.String}}">{{.LastError.String}}</span>{{end}} 13 + </div> 14 + </div> 15 + <div class="flex items-center gap-3 shrink-0"> 16 + <form hx-post="/feeds/retry" hx-target="#dead-feeds" hx-swap="outerHTML" class="inline"> 17 + <input type="hidden" name="url" value="{{.FeedURL}}"> 18 + <button type="submit" class="text-xs text-spot-secondary hover:text-spot-green transition font-bold uppercase">Retry</button> 19 + </form> 20 + <form hx-delete="/feeds/remove" hx-target="closest .dead-feed-item" hx-swap="outerHTML swap:0.3s" 21 + hx-confirm="Unsubscribe from this broken feed?" class="inline"> 22 + {{csrfInput $.CSRFToken}} 23 + <input type="hidden" name="url" value="{{.FeedURL}}"> 24 + <button type="submit" class="text-xs text-spot-secondary hover:text-spot-red transition font-bold uppercase">Unsubscribe</button> 25 + </form> 26 + </div> 27 + </div> 28 + {{end}} 29 + </div> 30 + </div> 31 + {{else}} 32 + <span></span> 33 + {{end}} 34 + {{end}}