mycurrencynet/mycurrencynet.go

182 lines
3.2 KiB
Go

package mycurrencynet
import (
"bytes"
"fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/go-resty/resty/v2"
"git.gensokyo.cafe/kkyy/mycurrencynet/iso"
)
var (
updateUrl = "https://www.mycurrency.net/=US"
client = resty.New()
)
type Currency struct {
Code string
Name string
Countries []string // TODO fill this
Rate float64
}
func (c *Currency) To(t *Currency) float64 {
if c == nil || t == nil || c.Rate == 0 {
return 0
}
return t.Rate / c.Rate
}
func init() {
client.
SetTimeout(5*time.Second).
SetRetryCount(1).
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0")
}
func fetch() (html []byte, err error) {
resp, err := client.R().Get(updateUrl)
if err != nil {
return
}
if resp.StatusCode() != 200 {
return nil, fmt.Errorf("HTTP %d %s: %q", resp.StatusCode(), resp.Status(), resp.String())
}
return resp.Body(), nil
}
func parse(body []byte) (map[string]*Currency, error) {
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(body))
if err != nil {
return nil, err
}
ret := make(map[string]*Currency)
doc.Find("tr.country").Each(func(_ int, s *goquery.Selection) {
var (
c Currency
isoC iso.ISOCurrency
ok bool
err error
rate string
)
if c.Code, ok = s.Attr("data-currency-code"); !ok {
return
}
if _, exist := ret[c.Code]; exist {
return
}
if isoC, ok = iso.Currencies[c.Code]; !ok {
return
}
c.Name = isoC.Name
if rate, ok = s.Find(".money[data-rate]").Attr("data-rate"); !ok {
return
}
if c.Rate, err = strconv.ParseFloat(rate, 64); err != nil {
return
}
ret[c.Code] = &c
})
return ret, nil
}
func Fetch() (map[string]*Currency, error) {
html, err := fetch()
if err != nil {
return nil, err
}
rates, err := parse(html)
if err != nil {
return nil, err
}
// add base currency
rates["USD"] = &Currency{
Code: "USD",
Name: iso.Currencies["USD"].Name,
Rate: 1,
}
return rates, nil
}
type MyCurrencyNet struct {
rates map[string]*Currency
lastUpdate time.Time
mu sync.RWMutex
}
func New() *MyCurrencyNet {
return &MyCurrencyNet{}
}
func (m *MyCurrencyNet) Get(codes ...string) ([]*Currency, error) {
if m.rates == nil {
return nil, fmt.Errorf("not initialized.")
}
ret := make([]*Currency, len(codes))
m.mu.RLock()
defer m.mu.RUnlock()
var (
errors []string
err error = nil
)
for i, code := range codes {
rate, ok := m.rates[strings.ToUpper(code)]
if !ok {
errors = append(errors, "unknown currency code: "+code)
continue
}
var cp Currency = *rate
ret[i] = &cp
}
if len(errors) > 0 {
err = fmt.Errorf(strings.Join(errors, ", "))
}
return ret, err
}
func (m *MyCurrencyNet) Update() error {
currencies, err := Fetch()
if err != nil {
return err
}
m.mu.Lock()
defer m.mu.Unlock()
m.rates = currencies
m.lastUpdate = time.Now()
return nil
}
func (m *MyCurrencyNet) UpdateEvery(interval time.Duration, onErr func(error), onSuccess func()) {
for {
<-time.After(interval)
if err := m.Update(); err != nil {
if onErr != nil {
onErr(err)
}
} else {
if onSuccess != nil {
onSuccess()
}
}
}
}