commit 811261d28358f66044b00a0bfa6771c4b436bd6f from: Matthew Streatfield via: GitHub date: Wed Apr 30 14:05:21 2025 UTC Merge pull request #9 from ollytom/main rss: handle more timestamp variants commit - 88856714674aff9291364b5ac6e7deb736ad97bf commit + 811261d28358f66044b00a0bfa6771c4b436bd6f blob - d3ffaca7b965399a05cccfa1cff212384af995e4 blob + 26432ce9fef20e60cd0a7487436512d3f5c17884 --- README.md +++ README.md @@ -1,3 +1,5 @@ +[![godoc](https://pkg.go.dev/static/frontend/badge/badge.svg)](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 @@ -2,6 +2,7 @@ package db import ( "encoding/json" + "errors" "github.com/streatCodes/rss/rss" bolt "go.etcd.io/bbolt" @@ -27,12 +28,17 @@ func (db *DB) SaveChannel(url string, channel *rss.Cha 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 @@ -1,6 +1,7 @@ package service import ( + "fmt" "html/template" "log" "net/http" @@ -32,7 +33,11 @@ func (service *Service) searchHandler(w http.ResponseW 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 { @@ -49,12 +54,17 @@ func (service *Service) channelHandler(w http.Response 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 @@ -0,0 +1,8 @@ +{{ define "error" }} +{{ template "header" }} +
+

Error

+

{{ . }}

+
+{{ template "footer" }} +{{ end }} blob - 4764ad87ee5f6482c61abe96e62f5e5ccb9c5bd7 blob + 40774a56b9b62f7948631610296f458396dd5387 --- rss/rss.go +++ rss/rss.go @@ -2,10 +2,14 @@ package rss 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"` @@ -44,16 +48,16 @@ func (ch *Channel) UnmarshalXML(d *xml.Decoder, start } 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 } @@ -98,9 +102,9 @@ func (it *Item) UnmarshalXML(d *xml.Decoder, start xml 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 } @@ -118,3 +122,19 @@ func Decode(r io.Reader) (*RSS, error) { } 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 @@ -61,3 +61,20 @@ func TestEmpty(t *testing.T) { 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) + } + }) + } +}