apps/bookmarks/favicon.go 1.7 K raw
1
package main
2
3
import (
4
	"context"
5
	"io"
6
	"net/http"
7
	"net/url"
8
	"strings"
9
	"time"
10
11
	"golang.org/x/net/html"
12
)
13
14
const faviconUA = "andromeda-bookmarks/0.1 (+https://github.com/stevedylandev/andromeda)"
15
16
func discoverFavicon(ctx context.Context, pageURL string) string {
17
	parsed, err := url.Parse(pageURL)
18
	if err != nil {
19
		return ""
20
	}
21
	client := &http.Client{Timeout: 15 * time.Second}
22
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, pageURL, nil)
23
	if err != nil {
24
		return ""
25
	}
26
	req.Header.Set("User-Agent", faviconUA)
27
	if resp, err := client.Do(req); err == nil {
28
		defer resp.Body.Close()
29
		body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
30
		if href := findFaviconHref(string(body)); href != "" {
31
			if u, err := parsed.Parse(href); err == nil {
32
				return u.String()
33
			}
34
		}
35
	}
36
	if u, err := parsed.Parse("/favicon.ico"); err == nil {
37
		return u.String()
38
	}
39
	return ""
40
}
41
42
func findFaviconHref(doc string) string {
43
	node, err := html.Parse(strings.NewReader(doc))
44
	if err != nil {
45
		return ""
46
	}
47
	wants := []string{"icon", "shortcut icon", "apple-touch-icon"}
48
	var found string
49
	var walk func(*html.Node)
50
	walk = func(n *html.Node) {
51
		if found != "" {
52
			return
53
		}
54
		if n.Type == html.ElementNode && strings.EqualFold(n.Data, "link") {
55
			rel, href := "", ""
56
			for _, a := range n.Attr {
57
				switch strings.ToLower(a.Key) {
58
				case "rel":
59
					rel = strings.ToLower(strings.TrimSpace(a.Val))
60
				case "href":
61
					href = a.Val
62
				}
63
			}
64
			for _, want := range wants {
65
				if rel == want {
66
					if href != "" {
67
						found = href
68
					}
69
					return
70
				}
71
			}
72
		}
73
		for c := n.FirstChild; c != nil; c = c.NextSibling {
74
			walk(c)
75
		}
76
	}
77
	walk(node)
78
	return found
79
}