Skip to content
This repository has been archived by the owner on Jun 26, 2021. It is now read-only.

Commit

Permalink
init favicon
Browse files Browse the repository at this point in the history
solve #895
  • Loading branch information
hellodword committed Mar 8, 2021
0 parents commit d65ed5a
Show file tree
Hide file tree
Showing 15 changed files with 410 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.vscode
.idea
favicon
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Favicon


| 阅读器 | 支持情况 | UA |
| --- | --- | --- |
| Feedbro | **Favicon provider** <br> [duckduckgo](https://icons.duckduckgo.com/ip3/mzi1otiznjm5nw.favicon.privacyhide.com.ico) 很容易不支持 <br> [google](https://www.google.com/s2/favicons?domain=mza3otc1oduxng.favicon.privacyhide.com) 支持好很多 | 浏览器的 UA |
| Reeder 4 | 不支持,但是会选取占比高的颜色生成色块 | `Reeder/4020.89.01` <br> `Reeder/4020.69.02` |
| Reeder 3 | 支持 | `Reeder/3.2.40` |
| Newsify | 支持 | `Newsify/469` |
| RSS Feed Reader (Chrome) | 支持 | |
| inoreader | 不支持 | |
| NetNewsWire | 支持 | |
| Tiny Tiny RSS | 未测试 | `Tiny Tiny RSS/21.03` |
| irreader | 未测试 | `irreader/1.5.9` |
| Miniflux | 未测试 | `Miniflux/2.0.28` |


![Feedbro](img/feedbro.png)
![Reeder 3](img/reeder3.jpg)
![Reeder 4](img/reeder4.jpg)
![Newsify](img/newsify.jpg)
![NetNewsWire](img/netnewsfire.jpg)

TODO:

- [ ] 将 png 转为 ico
- [ ] 优化 favicon 访问速度
- [ ] 兼容更多阅读器

42 changes: 42 additions & 0 deletions fetch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package main

import (
"context"
"encoding/json"
"io/ioutil"
"net/http"
)

func fetch(ctx context.Context, u string) ([]byte, error) {
request, err := http.NewRequestWithContext(ctx, "GET", u, nil)
if err != nil {
return nil, err
}

response, err := (&http.Client{}).Do(request)
if err != nil {
return nil, err
}

defer response.Body.Close()
return ioutil.ReadAll(response.Body)
}

type Detail struct {
//Name string `json:"name"`
Bizid string `json:"bizid"`
//Description string `json:"description"`
HeadImg string `json:"head_img"`
//LastUpdate int64 `json:"last_update"`
}

func fetchDetails(ctx context.Context) ([]Detail, error) {
body, err := fetch(ctx, "https://raw.githubusercontent.com/hellodword/wechat-feeds/feeds/details.json")
if err != nil {
return nil, err
}

var details []Detail
err = json.Unmarshal(body, &details)
return details, err
}
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module favicon

go 1.15

require (
github.com/h2non/bimg v1.1.5
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
github.com/h2non/bimg v1.1.5 h1:o3xsUBxM8s7+e7PmpiWIkEYdeYayJ94eh4cJLx67m1k=
github.com/h2non/bimg v1.1.5/go.mod h1:R3+UiYwkK4rQl6KVFTOFJHitgLbZXBZNFh2cv3AEbp8=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
93 changes: 93 additions & 0 deletions handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package main

import (
"fmt"
"log"
"net/http"
"strconv"
"strings"
)

func writeFavicon(w http.ResponseWriter, name string, bApple bool) {
var favicon interface{}
var ok bool
if bApple {
favicon, ok = mapFaviconApple.Load(name)
} else {
favicon, ok = mapFaviconNormal.Load(name)
}

if !ok {
writeError(w)
return
}

img := favicon.([]byte) // formatImage(favicon.([]byte), bApple)
// https://stackoverflow.com/questions/56131723/in-http-response-for-a-chunked-data-how-to-set-content-length
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Content-Length", strconv.Itoa(len(img)))
//w.WriteHeader(http.StatusOK)

_, err := w.Write(img)
if err != nil {
log.Println("fmt.Fprintf", err)
}

}

func writeError(w http.ResponseWriter) {
w.WriteHeader(http.StatusBadRequest)
_, err := fmt.Fprint(w, "bad request")

if err != nil {
log.Println("fmt.Fprintf", err)
}
}

func handleFavicon(w http.ResponseWriter, r *http.Request) {

w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Add("Access-Control-Allow-Headers", "*")
w.Header().Add("Access-Control-Allow-Methods", "*")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}

bApple := r.URL.Query().Get("apple") != ""

bizid := formatBizID(strings.Split(r.URL.Query().Get("host"), ".")[0])
if bizid == "" || bizid == DefaultFavicon {
writeFavicon(w, DefaultFavicon, bApple)
return
}

var err error
_, ok := mapFaviconNormal.Load(bizid)
if ok {
writeFavicon(w, bizid, bApple)
return
}

headImg, ok := mapHeadImg.Load(bizid)
if !ok {
_ = syncHeadImgs(r.Context())
headImg, ok = mapHeadImg.Load(bizid)
}

if !ok {
writeError(w)
return
}

img, err := fetch(r.Context(), headImg.(string))
if err != nil {
writeError(w)
return
}

mapFaviconNormal.Store(bizid, formatAndResize(img, SizeNormal))
mapFaviconApple.Store(bizid, formatAndResize(img, SizeApple))

writeFavicon(w, bizid, bApple)
}
75 changes: 75 additions & 0 deletions image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package main

import (
"bytes"
"fmt"
"github.com/h2non/bimg"
"image/jpeg"
"image/png"
"math"
"net/http"
)

// ToPng converts an image to png
// https://gist.github.com/tizz98/fb15f8dd0c55ac8d2be0e3c4bd8249c3
func ToPng(imageBytes []byte) ([]byte, error) {
contentType := http.DetectContentType(imageBytes)

switch contentType {
case "image/png":
case "image/jpeg":
img, err := jpeg.Decode(bytes.NewReader(imageBytes))
if err != nil {
return nil, err
}

buf := new(bytes.Buffer)
if err := png.Encode(buf, img); err != nil {
return nil, err
}

return buf.Bytes(), nil
}

return nil, fmt.Errorf("unable to convert %#v to png", contentType)
}

func formatAndResize(b []byte, size int) []byte {
var err error

bPng, _ := ToPng(b)
if bPng != nil {
b = bPng
}

img := bimg.NewImage(b)
if img.Type() != bimg.ImageTypeName(bimg.PNG) {
var newImage []byte
newImage, err = img.Convert(bimg.PNG)
if err != nil {
return b
}
img = bimg.NewImage(newImage)
}

inputSize, err := img.Size()
if err != nil {
return b
}

if inputSize.Width != inputSize.Height {
return b
}

if size > inputSize.Width {
size = int(math.Min(SizeNormal, float64(inputSize.Width)))
}

newIcon, err := img.Resize(size, size)

if err != nil {
return b
} else {
return newIcon
}
}
Binary file added img/feedbro.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/netnewsfire.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/newsify.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/reeder3.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/reeder4.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
94 changes: 94 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package main

import (
"context"
"fmt"
"io/ioutil"
"net/http"
"strings"
"sync"
)

const (
DefaultFavicon = "default"
)

const (
SizeNormal = 32 //196
SizeApple = 57 //180
)

var mapFaviconNormal sync.Map
var mapFaviconApple sync.Map
var mapHeadImg sync.Map

func init() {

r, err := http.Get("https://wechat.privacyhide.com/favicon.ico")
if err != nil {
panic(err)
}

if r.StatusCode != http.StatusOK {
panic(fmt.Errorf("status code %d", r.StatusCode))
}

defer r.Body.Close()
b, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}

contentType := http.DetectContentType(b)
if contentType != "image/x-icon" {
panic(fmt.Errorf("content type %s", contentType))
}

mapFaviconNormal.Store(DefaultFavicon, b)

err = syncHeadImgs(context.Background())
if err != nil {
panic(err)
}

}

func syncHeadImgs(ctx context.Context) error {
details, err := fetchDetails(ctx)
if err != nil {
return err
}

for i := range details {
if details[i].HeadImg != "" {
mapHeadImg.Store(formatBizID(details[i].Bizid), formatHeadImg(details[i].HeadImg))
}
}
return nil
}

func formatHeadImg(headImg string) string {
if strings.HasSuffix(headImg, "/132") {
headImg = strings.TrimSuffix(headImg, "/132")
headImg = headImg + "/64"
}
return headImg
}

func formatBizID(bizid string) string {
return strings.ToLower(strings.ReplaceAll(bizid, "=", ""))
}

func main() {

handler := http.NewServeMux()
handler.HandleFunc("/favicon.ico", handleFavicon)

server := &http.Server{
Addr: ":8080",
Handler: handler,
}

panic(server.ListenAndServe())

}
31 changes: 31 additions & 0 deletions nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
http {
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=STATIC:50m inactive=6h max_size=2g;

server {
listen 80;
server_name *.favicon.privacyhide.com;

resolver 1.1.1.1 8.8.8.8;

location / {
break;
}

location ~^/favicon.ico$ {
proxy_cache STATIC;
proxy_ignore_headers X-Accel-Expires Expires Cache-Control;
proxy_cache_valid 200 6h;
proxy_read_timeout 5m;
proxy_pass "http://127.0.0.1:8080/favicon.ico?host=$host";
}

location ~^/apple-.*.png$ {
proxy_cache STATIC;
proxy_ignore_headers X-Accel-Expires Expires Cache-Control;
proxy_cache_valid 200 6h;
proxy_read_timeout 5m;
proxy_pass "http://127.0.0.1:8080/favicon.ico?host=$host&apple=1";
}
}
}

Loading

0 comments on commit d65ed5a

Please sign in to comment.