Commit Diff


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" }}
+<main>
+<h2>Error</h2>
+<p>{{ . }}</p>
+</main>
+{{ 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)
+			}
+		})
+	}
+}