commit - 88856714674aff9291364b5ac6e7deb736ad97bf
commit + 811261d28358f66044b00a0bfa6771c4b436bd6f
blob - d3ffaca7b965399a05cccfa1cff212384af995e4
blob + 26432ce9fef20e60cd0a7487436512d3f5c17884
--- README.md
+++ README.md
+[](https://pkg.go.dev/github.com/StreatCodes/rss)
+
## RSS
Search RSS feeds and collate a feed.
blob - e13d3e5bec8d3e544e132b865037605b3202a231
blob + d703d35406a6ac437561fcde96b4bc3023beb867
--- internal/db/channels.go
+++ internal/db/channels.go
import (
"encoding/json"
+ "errors"
"github.com/streatCodes/rss/rss"
bolt "go.etcd.io/bbolt"
return err
}
+var ErrNotExist = errors.New("channel does not exist")
+
func (db *DB) GetChannel(url string) (*rss.Channel, error) {
var channelBytes []byte
err := db.raw.View(func(tx *bolt.Tx) error {
if bucket := tx.Bucket(channelsBucket); bucket != nil {
channelBytes = bucket.Get([]byte(url))
}
+ if len(channelBytes) == 0 {
+ return ErrNotExist
+ }
return nil
})
blob - 60238e8a438264893569d426e11a67367d81bc03
blob + b61e87ec0010a3943819ef43bf5d601f5796072f
--- internal/service/handlers.go
+++ internal/service/handlers.go
package service
import (
+ "fmt"
"html/template"
"log"
"net/http"
results, err := service.findChannel(searchQuery)
if err != nil {
- panic("TODO")
+ msg := fmt.Sprintf("find %q: %v", searchQuery, err)
+ log.Println(msg)
+ w.WriteHeader(http.StatusInternalServerError)
+ render(w, "error", msg)
+ return
}
if isHtmx {
channelPage := ChannelResult{ShowSubscribeButton: true}
channelUrl := strings.TrimPrefix(r.URL.Path, "/channel/")
- //Check to see if we have the feed in the database
- if channel, err := service.db.GetChannel(channelUrl); channel != nil && err == nil {
- channelPage.Channel = *channel
+ // Check to see if we have the feed in the database
+ channel, err := service.db.GetChannel(channelUrl)
+ if err != nil {
+ msg := fmt.Sprintf("get %q: %v", channelUrl, err)
+ log.Println(msg)
+ w.WriteHeader(http.StatusInternalServerError)
+ render(w, "error", msg)
+ return
}
-
- err := render(w, "channelPage", channelPage)
+ channelPage.Channel = *channel
+ err = render(w, "channelPage", channelPage)
if err != nil {
log.Printf("Error executing template - %s", err)
}
blob - /dev/null
blob + c0063f254eb9a0bc787e58bc08a0c92beab96e71 (mode 644)
--- /dev/null
+++ internal/templates/error.tmpl
+{{ define "error" }}
+{{ template "header" }}
+<main>
+<h2>Error</h2>
+<p>{{ . }}</p>
+</main>
+{{ template "footer" }}
+{{ end }}
blob - 4764ad87ee5f6482c61abe96e62f5e5ccb9c5bd7
blob + 40774a56b9b62f7948631610296f458396dd5387
--- rss/rss.go
+++ rss/rss.go
import (
"encoding/xml"
+ "fmt"
"io"
"time"
)
+// MediaType is RSS' MIME media type.
+const MediaType = "application/rss+xml"
+
type RSS struct {
Version string `xml:"version,attr"`
Channel Channel `xml:"channel"`
}
if aux.PubDate != "" {
- t, err := time.Parse(time.RFC1123Z, aux.PubDate)
+ t, err := parseTime(aux.PubDate)
if err != nil {
- return err
+ return fmt.Errorf("parse published date %q: %w", aux.PubDate, err)
}
ch.PubDate = t
}
if aux.LastBuildDate != "" {
- t, err := time.Parse(time.RFC1123Z, aux.LastBuildDate)
+ t, err := parseTime(aux.LastBuildDate)
if err != nil {
- return err
+ return fmt.Errorf("parse last build date %q: %w", aux.LastBuildDate, err)
}
ch.LastBuildDate = t
}
return err
}
if aux.PubDate != "" {
- t, err := time.Parse(time.RFC1123Z, aux.PubDate)
+ t, err := parseTime(aux.PubDate)
if err != nil {
- return err
+ return fmt.Errorf("parse published date %q: %w", aux.PubDate, err)
}
it.PubDate = t
}
}
return &rss, nil
}
+
+func parseTime(s string) (time.Time, error) {
+ layouts := []string{
+ time.RFC1123Z, time.RFC1123,
+ time.RFC822Z, time.RFC822,
+ "Mon, _2 Jan 2006 15:04:05 -0700", // rfc1123z with no trailing zero
+ "Mon, _2 January 2006 15:04:05 -0700", // long month name
+ }
+ for _, l := range layouts {
+ t, err := time.Parse(l, s)
+ if err == nil {
+ return t, nil
+ }
+ }
+ return time.Time{}, fmt.Errorf("unsupported layout")
+}
blob - f2cf2b7c74ea4e2ea0fe2d68bcaa70f6e51dfa26
blob + b194fe55a25f5e30dfa9aa05d351b3d5c843e5cc
--- rss/rss_test.go
+++ rss/rss_test.go
t.Fatal(err)
}
}
+
+func TestParseTime(t *testing.T) {
+ var tests = []struct {
+ name string
+ timestamp string
+ }{
+ {"no leading zero", "Fri, 4 Feb 2022 09:30:00 +1300"}, // https://benhoyt.com/writings/rss.xml
+ {"long month", "Mon, 10 June 2024 12:20:00 +0000"}, // https://www.claws-mail.org/releases.rss
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if _, err := parseTime(tt.timestamp); err != nil {
+ t.Errorf("parse %q: %v", tt.timestamp, err)
+ }
+ })
+ }
+}