Restructure & refactor codebase
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
package glance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"math"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
var marketsWidgetTemplate = mustParseTemplate("markets.html", "widget-base.html")
|
||||
|
||||
type marketsWidget struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
StocksRequests []marketRequest `yaml:"stocks"`
|
||||
MarketRequests []marketRequest `yaml:"markets"`
|
||||
Sort string `yaml:"sort-by"`
|
||||
Markets marketList `yaml:"-"`
|
||||
}
|
||||
|
||||
func (widget *marketsWidget) initialize() error {
|
||||
widget.withTitle("Markets").withCacheDuration(time.Hour)
|
||||
|
||||
// legacy support, remove in v0.10.0
|
||||
if len(widget.MarketRequests) == 0 {
|
||||
widget.MarketRequests = widget.StocksRequests
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (widget *marketsWidget) update(ctx context.Context) {
|
||||
markets, err := fetchMarketsDataFromYahoo(widget.MarketRequests)
|
||||
|
||||
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
||||
return
|
||||
}
|
||||
|
||||
if widget.Sort == "absolute-change" {
|
||||
markets.sortByAbsChange()
|
||||
}
|
||||
|
||||
if widget.Sort == "change" {
|
||||
markets.sortByChange()
|
||||
}
|
||||
|
||||
widget.Markets = markets
|
||||
}
|
||||
|
||||
func (widget *marketsWidget) Render() template.HTML {
|
||||
return widget.renderTemplate(widget, marketsWidgetTemplate)
|
||||
}
|
||||
|
||||
type marketRequest struct {
|
||||
Name string `yaml:"name"`
|
||||
Symbol string `yaml:"symbol"`
|
||||
ChartLink string `yaml:"chart-link"`
|
||||
SymbolLink string `yaml:"symbol-link"`
|
||||
}
|
||||
|
||||
type market struct {
|
||||
marketRequest
|
||||
Currency string
|
||||
Price float64
|
||||
PercentChange float64
|
||||
SvgChartPoints string
|
||||
}
|
||||
|
||||
type marketList []market
|
||||
|
||||
func (t marketList) sortByAbsChange() {
|
||||
sort.Slice(t, func(i, j int) bool {
|
||||
return math.Abs(t[i].PercentChange) > math.Abs(t[j].PercentChange)
|
||||
})
|
||||
}
|
||||
|
||||
func (t marketList) sortByChange() {
|
||||
sort.Slice(t, func(i, j int) bool {
|
||||
return t[i].PercentChange > t[j].PercentChange
|
||||
})
|
||||
}
|
||||
|
||||
type marketResponseJson struct {
|
||||
Chart struct {
|
||||
Result []struct {
|
||||
Meta struct {
|
||||
Currency string `json:"currency"`
|
||||
Symbol string `json:"symbol"`
|
||||
RegularMarketPrice float64 `json:"regularMarketPrice"`
|
||||
ChartPreviousClose float64 `json:"chartPreviousClose"`
|
||||
} `json:"meta"`
|
||||
Indicators struct {
|
||||
Quote []struct {
|
||||
Close []float64 `json:"close,omitempty"`
|
||||
} `json:"quote"`
|
||||
} `json:"indicators"`
|
||||
} `json:"result"`
|
||||
} `json:"chart"`
|
||||
}
|
||||
|
||||
// TODO: allow changing chart time frame
|
||||
const marketChartDays = 21
|
||||
|
||||
func fetchMarketsDataFromYahoo(marketRequests []marketRequest) (marketList, error) {
|
||||
requests := make([]*http.Request, 0, len(marketRequests))
|
||||
|
||||
for i := range marketRequests {
|
||||
request, _ := http.NewRequest("GET", fmt.Sprintf("https://query1.finance.yahoo.com/v8/finance/chart/%s?range=1mo&interval=1d", marketRequests[i].Symbol), nil)
|
||||
requests = append(requests, request)
|
||||
}
|
||||
|
||||
job := newJob(decodeJsonFromRequestTask[marketResponseJson](defaultClient), requests)
|
||||
responses, errs, err := workerPoolDo(job)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", errNoContent, err)
|
||||
}
|
||||
|
||||
markets := make(marketList, 0, len(responses))
|
||||
var failed int
|
||||
|
||||
for i := range responses {
|
||||
if errs[i] != nil {
|
||||
failed++
|
||||
slog.Error("Failed to fetch market data", "symbol", marketRequests[i].Symbol, "error", errs[i])
|
||||
continue
|
||||
}
|
||||
|
||||
response := responses[i]
|
||||
|
||||
if len(response.Chart.Result) == 0 {
|
||||
failed++
|
||||
slog.Error("Market response contains no data", "symbol", marketRequests[i].Symbol)
|
||||
continue
|
||||
}
|
||||
|
||||
prices := response.Chart.Result[0].Indicators.Quote[0].Close
|
||||
|
||||
if len(prices) > marketChartDays {
|
||||
prices = prices[len(prices)-marketChartDays:]
|
||||
}
|
||||
|
||||
previous := response.Chart.Result[0].Meta.RegularMarketPrice
|
||||
|
||||
if len(prices) >= 2 && prices[len(prices)-2] != 0 {
|
||||
previous = prices[len(prices)-2]
|
||||
}
|
||||
|
||||
points := svgPolylineCoordsFromYValues(100, 50, maybeCopySliceWithoutZeroValues(prices))
|
||||
|
||||
currency, exists := currencyToSymbol[response.Chart.Result[0].Meta.Currency]
|
||||
|
||||
if !exists {
|
||||
currency = response.Chart.Result[0].Meta.Currency
|
||||
}
|
||||
|
||||
markets = append(markets, market{
|
||||
marketRequest: marketRequests[i],
|
||||
Price: response.Chart.Result[0].Meta.RegularMarketPrice,
|
||||
Currency: currency,
|
||||
PercentChange: percentChange(
|
||||
response.Chart.Result[0].Meta.RegularMarketPrice,
|
||||
previous,
|
||||
),
|
||||
SvgChartPoints: points,
|
||||
})
|
||||
}
|
||||
|
||||
if len(markets) == 0 {
|
||||
return nil, errNoContent
|
||||
}
|
||||
|
||||
if failed > 0 {
|
||||
return markets, fmt.Errorf("%w: could not fetch data for %d market(s)", errPartialContent, failed)
|
||||
}
|
||||
|
||||
return markets, nil
|
||||
}
|
||||
|
||||
var currencyToSymbol = map[string]string{
|
||||
"USD": "$",
|
||||
"EUR": "€",
|
||||
"JPY": "¥",
|
||||
"CAD": "C$",
|
||||
"AUD": "A$",
|
||||
"GBP": "£",
|
||||
"CHF": "Fr",
|
||||
"NZD": "N$",
|
||||
"INR": "₹",
|
||||
"BRL": "R$",
|
||||
"RUB": "₽",
|
||||
"TRY": "₺",
|
||||
"ZAR": "R",
|
||||
"CNY": "¥",
|
||||
"KRW": "₩",
|
||||
"HKD": "HK$",
|
||||
"SGD": "S$",
|
||||
"SEK": "kr",
|
||||
"NOK": "kr",
|
||||
"DKK": "kr",
|
||||
"PLN": "zł",
|
||||
"PHP": "₱",
|
||||
}
|
||||
Reference in New Issue
Block a user