diff --git a/server/crawler.go b/server/crawler.go index 99ba9b0..75e40bc 100644 --- a/server/crawler.go +++ b/server/crawler.go @@ -1,7 +1,9 @@ package server import ( + "fmt" "github.com/PuerkitoBio/goquery" + "io/ioutil" "net/http" "net/url" ) @@ -32,3 +34,60 @@ func FindFeeds(r *http.Response) ([]FeedSource, error) { }) return sources, nil } + +func findFavicon(websiteUrl, feedUrl string) (*[]byte, error) { + candidateUrls := make([]string, 0) + + favicon := func(link string) string { + u, err := url.Parse(link) + if err != nil { + return "" + } + return fmt.Sprintf("%s://%s/favicon.ico", u.Scheme, u.Host) + } + + if len(websiteUrl) != 0 { + doc, err := goquery.NewDocument(websiteUrl) + if err != nil { + return nil, err + } + doc.Find(`link[rel=icon]`).EachWithBreak(func(i int, s *goquery.Selection) bool { + if href, ok := s.Attr("href"); ok { + if hrefUrl, err := url.Parse(href); err == nil { + faviconUrl := doc.Url.ResolveReference(hrefUrl).String() + candidateUrls = append(candidateUrls, faviconUrl) + } + } + return true + }) + + if c := favicon(websiteUrl); len(c) != 0 { + candidateUrls = append(candidateUrls, c) + } + } + if c := favicon(feedUrl); len(c) != 0 { + candidateUrls = append(candidateUrls, c) + } + + client := http.Client{} + + imageTypes := [4]string{ + "image/x-icon", + "image/png", + "image/jpeg", + "image/gif", + } + for _, url := range candidateUrls { + if res, err := client.Get(url); err == nil && res.StatusCode == 200 { + if content, err := ioutil.ReadAll(res.Body); err == nil { + ctype := http.DetectContentType(content) + for _, itype := range imageTypes { + if ctype == itype { + return &content, nil + } + } + } + } + } + return nil, nil +} diff --git a/server/handlers.go b/server/handlers.go index bcd9193..2bb186b 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -27,9 +27,10 @@ var routes []Route = []Route{ p("/api/folders", FolderListHandler), p("/api/folders/:id", FolderHandler), p("/api/feeds", FeedListHandler), - p("/api/feeds/refresh", FeedRefreshHandler), - p("/api/feeds/:id", FeedHandler), p("/api/feeds/find", FeedHandler), + p("/api/feeds/refresh", FeedRefreshHandler), + p("/api/feeds/:id/icon", FeedIconHandler), + p("/api/feeds/:id", FeedHandler), p("/api/items", ItemListHandler), p("/api/items/:id", ItemHandler), p("/api/settings", SettingsHandler), @@ -146,6 +147,22 @@ func FeedRefreshHandler(rw http.ResponseWriter, req *http.Request) { } } +func FeedIconHandler(rw http.ResponseWriter, req *http.Request) { + id, err := strconv.ParseInt(Vars(req)["id"], 10, 64) + if err != nil { + rw.WriteHeader(http.StatusBadRequest) + return + } + feed := db(req).GetFeed(id) + if feed != nil && feed.Icon != nil { + rw.Header().Set("Content-Type", http.DetectContentType(*feed.Icon)) + rw.Header().Set("Content-Length", strconv.Itoa(len(*feed.Icon))) + rw.Write(*feed.Icon) + } else { + rw.WriteHeader(http.StatusNotFound) + } +} + func FeedListHandler(rw http.ResponseWriter, req *http.Request) { if req.Method == "GET" { list := db(req).ListFeeds() @@ -259,7 +276,6 @@ func createFeed(s *storage.Storage, url string, folderId *int64) error { feed.Description, feed.Link, feedLink, - "", folderId, ) s.CreateItems(convertItems(feed.Items, *storedFeed)) @@ -430,11 +446,11 @@ func OPMLImportHandler(rw http.ResponseWriter, req *http.Request) { } for _, outline := range feeds.Outlines { if outline.Type == "rss" { - db(req).CreateFeed(outline.Title, outline.Description, outline.SiteURL, outline.FeedURL, "", nil) + db(req).CreateFeed(outline.Title, outline.Description, outline.SiteURL, outline.FeedURL, nil) } else { folder := db(req).CreateFolder(outline.Title) for _, o := range outline.AllFeeds() { - db(req).CreateFeed(o.Title, o.Description, o.SiteURL, o.FeedURL, "", &folder.Id) + db(req).CreateFeed(o.Title, o.Description, o.SiteURL, o.FeedURL, &folder.Id) } } } diff --git a/server/server.go b/server/server.go index 5d54e04..d4b87fe 100644 --- a/server/server.go +++ b/server/server.go @@ -53,6 +53,15 @@ func (h *Handler) startJobs() { items := listItems(feed) h.db.CreateItems(items) atomic.AddInt32(h.queueSize, -1) + if !feed.HasIcon { + icon, err := findFavicon(feed.Link, feed.FeedLink) + if icon != nil { + h.db.UpdateFeedIcon(feed.Id, icon) + } + if err != nil { + h.log.Print(err) + } + } case <- delTicker.C: h.db.DeleteOldItems() } diff --git a/storage/feed.go b/storage/feed.go index 7ab3e30..09c9e68 100644 --- a/storage/feed.go +++ b/storage/feed.go @@ -5,21 +5,22 @@ import ( ) type Feed struct { - Id int64 `json:"id"` - FolderId *int64 `json:"folder_id"` - Title string `json:"title"` - Description string `json:"description"` - Link string `json:"link"` - FeedLink string `json:"feed_link"` - Icon string `json:"icon"` + Id int64 `json:"id"` + FolderId *int64 `json:"folder_id"` + Title string `json:"title"` + Description string `json:"description"` + Link string `json:"link"` + FeedLink string `json:"feed_link"` + Icon *[]byte `json:"icon,omitempty"` + HasIcon bool `json:"has_icon"` } -func (s *Storage) CreateFeed(title, description, link, feedLink, icon string, folderId *int64) *Feed { +func (s *Storage) CreateFeed(title, description, link, feedLink string, folderId *int64) *Feed { result, err := s.db.Exec(` - insert into feeds (title, description, link, feed_link, icon, folder_id) - values (?, ?, ?, ?, ?, ?) + insert into feeds (title, description, link, feed_link, folder_id) + values (?, ?, ?, ?, ?) on conflict (feed_link) do update set folder_id=?`, - html.UnescapeString(title), description, link, feedLink, icon, folderId, + html.UnescapeString(title), description, link, feedLink, folderId, folderId, ) if err != nil { @@ -35,7 +36,6 @@ func (s *Storage) CreateFeed(title, description, link, feedLink, icon string, fo Description: description, Link: link, FeedLink: feedLink, - Icon: icon, FolderId: folderId, } } @@ -56,10 +56,16 @@ func (s *Storage) UpdateFeedFolder(feedId int64, newFolderId int64) bool { return err == nil } +func (s *Storage) UpdateFeedIcon(feedId int64, icon *[]byte) bool { + _, err := s.db.Exec(`update feeds set icon = ? where id = ?`, icon, feedId) + return err == nil +} + func (s *Storage) ListFeeds() []Feed { result := make([]Feed, 0, 0) rows, err := s.db.Query(` - select id, folder_id, title, description, link, feed_link, icon + select id, folder_id, title, description, link, feed_link, + ifnull(icon, '') != '' as has_icon from feeds `) if err != nil { @@ -75,7 +81,7 @@ func (s *Storage) ListFeeds() []Feed { &f.Description, &f.Link, &f.FeedLink, - &f.Icon, + &f.HasIcon, ) if err != nil { s.log.Print(err) @@ -85,3 +91,26 @@ func (s *Storage) ListFeeds() []Feed { } return result } + +func (s *Storage) GetFeed(id int64) *Feed { + row := s.db.QueryRow(` + select id, folder_id, title, description, link, feed_link, icon, + ifnull(icon, '') != '' as has_icon + from feeds where id = ? + `, id) + if row != nil { + var f Feed + row.Scan( + &f.Id, + &f.FolderId, + &f.Title, + &f.Description, + &f.Link, + &f.FeedLink, + &f.Icon, + &f.HasIcon, + ) + return &f + } + return nil +} diff --git a/storage/storage.go b/storage/storage.go index 2dc9410..bc2b178 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -23,7 +23,7 @@ create table if not exists feeds ( description text, link text, feed_link text not null, - icon text + icon blob ); create index if not exists idx_feed_folder_id on feeds(folder_id); diff --git a/template/index.html b/template/index.html index 3d77a54..6f15b74 100644 --- a/template/index.html +++ b/template/index.html @@ -114,7 +114,8 @@ v-for="feed in folder.feeds">
- {% inline "rss.svg" %} + {% inline "rss.svg" %} + {{ feed.title }} {{filteredFeedStats[feed.id] || ''}}
diff --git a/template/static/stylesheets/app.css b/template/static/stylesheets/app.css index f7ba6a7..334b3ac 100644 --- a/template/static/stylesheets/app.css +++ b/template/static/stylesheets/app.css @@ -141,7 +141,7 @@ select.form-control:not([multiple]):not([size]) { line-height: 1; } -.icon > svg { +.icon > svg , .icon > img { width: 1rem; height: 1rem; vertical-align: top;