Initial commit

This commit is contained in:
Svilen Markov
2024-04-27 20:10:24 +01:00
commit ec8ba40cf0
100 changed files with 6883 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
package widget
import (
"html/template"
"github.com/glanceapp/glance/internal/assets"
)
type Bookmarks struct {
widgetBase `yaml:",inline"`
cachedHTML template.HTML `yaml:"-"`
Groups []struct {
Title string `yaml:"title"`
Color *HSLColorField `yaml:"color"`
Links []struct {
Title string `yaml:"title"`
URL string `yaml:"url"`
} `yaml:"links"`
} `yaml:"groups"`
}
func (widget *Bookmarks) Initialize() error {
widget.withTitle("Bookmarks").withError(nil)
widget.cachedHTML = widget.render(widget, assets.BookmarksTemplate)
return nil
}
func (widget *Bookmarks) Render() template.HTML {
return widget.cachedHTML
}

View File

@@ -0,0 +1,30 @@
package widget
import (
"context"
"html/template"
"time"
"github.com/glanceapp/glance/internal/assets"
"github.com/glanceapp/glance/internal/feed"
)
type Calendar struct {
widgetBase `yaml:",inline"`
Calendar *feed.Calendar
}
func (widget *Calendar) Initialize() error {
widget.withTitle("Calendar").withCacheOnTheHour()
return nil
}
func (widget *Calendar) Update(ctx context.Context) {
widget.Calendar = feed.NewCalendar(time.Now())
widget.withError(nil).scheduleNextUpdate()
}
func (widget *Calendar) Render() template.HTML {
return widget.render(widget, assets.CalendarTemplate)
}

152
internal/widget/fields.go Normal file
View File

@@ -0,0 +1,152 @@
package widget
import (
"fmt"
"html/template"
"os"
"regexp"
"strconv"
"time"
"gopkg.in/yaml.v3"
)
var HSLColorPattern = regexp.MustCompile(`^(?:hsla?\()?(\d{1,3})(?: |,)+(\d{1,3})%?(?: |,)+(\d{1,3})%?\)?$`)
var EnvFieldPattern = regexp.MustCompile(`^\${([A-Z_]+)}$`)
const (
HSLHueMax = 360
HSLSaturationMax = 100
HSLLightnessMax = 100
)
type HSLColorField struct {
Hue uint16
Saturation uint8
Lightness uint8
}
func (c *HSLColorField) String() string {
return fmt.Sprintf("hsl(%d, %d%%, %d%%)", c.Hue, c.Saturation, c.Lightness)
}
func (c *HSLColorField) AsCSSValue() template.CSS {
return template.CSS(c.String())
}
func (c *HSLColorField) UnmarshalYAML(node *yaml.Node) error {
var value string
if err := node.Decode(&value); err != nil {
return err
}
matches := HSLColorPattern.FindStringSubmatch(value)
if len(matches) != 4 {
return fmt.Errorf("invalid HSL color format: %s", value)
}
hue, err := strconv.ParseUint(matches[1], 10, 16)
if err != nil {
return err
}
if hue > HSLHueMax {
return fmt.Errorf("HSL hue must be between 0 and %d", HSLHueMax)
}
saturation, err := strconv.ParseUint(matches[2], 10, 8)
if err != nil {
return err
}
if saturation > HSLSaturationMax {
return fmt.Errorf("HSL saturation must be between 0 and %d", HSLSaturationMax)
}
lightness, err := strconv.ParseUint(matches[3], 10, 8)
if err != nil {
return err
}
if lightness > HSLLightnessMax {
return fmt.Errorf("HSL lightness must be between 0 and %d", HSLLightnessMax)
}
c.Hue = uint16(hue)
c.Saturation = uint8(saturation)
c.Lightness = uint8(lightness)
return nil
}
var DurationPattern = regexp.MustCompile(`^(\d+)(s|m|h|d)$`)
type DurationField time.Duration
func (d *DurationField) UnmarshalYAML(node *yaml.Node) error {
var value string
if err := node.Decode(&value); err != nil {
return err
}
matches := DurationPattern.FindStringSubmatch(value)
if len(matches) != 3 {
return fmt.Errorf("invalid duration format: %s", value)
}
duration, err := strconv.Atoi(matches[1])
if err != nil {
return err
}
switch matches[2] {
case "s":
*d = DurationField(time.Duration(duration) * time.Second)
case "m":
*d = DurationField(time.Duration(duration) * time.Minute)
case "h":
*d = DurationField(time.Duration(duration) * time.Hour)
case "d":
*d = DurationField(time.Duration(duration) * 24 * time.Hour)
}
return nil
}
type OptionalEnvString string
func (f *OptionalEnvString) UnmarshalYAML(node *yaml.Node) error {
var value string
err := node.Decode(&value)
if err != nil {
return err
}
matches := EnvFieldPattern.FindStringSubmatch(value)
if len(matches) != 2 {
*f = OptionalEnvString(value)
return nil
}
value, found := os.LookupEnv(matches[1])
if !found {
return fmt.Errorf("environment variable %s not found", matches[1])
}
*f = OptionalEnvString(value)
return nil
}

View File

@@ -0,0 +1,52 @@
package widget
import (
"context"
"html/template"
"time"
"github.com/glanceapp/glance/internal/assets"
"github.com/glanceapp/glance/internal/feed"
)
type HackerNews struct {
widgetBase `yaml:",inline"`
Posts feed.ForumPosts `yaml:"-"`
Limit int `yaml:"limit"`
CollapseAfter int `yaml:"collapse-after"`
}
func (widget *HackerNews) Initialize() error {
widget.withTitle("Hacker News").withCacheDuration(30 * time.Minute)
if widget.Limit <= 0 {
widget.Limit = 15
}
if widget.CollapseAfter == 0 || widget.CollapseAfter < -1 {
widget.CollapseAfter = 5
}
return nil
}
func (widget *HackerNews) Update(ctx context.Context) {
posts, err := feed.FetchHackerNewsTopPosts(40)
if !widget.canContinueUpdateAfterHandlingErr(err) {
return
}
posts.CalculateEngagement()
posts.SortByEngagement()
if widget.Limit < len(posts) {
posts = posts[:widget.Limit]
}
widget.Posts = posts
}
func (widget *HackerNews) Render() template.HTML {
return widget.render(widget, assets.ForumPostsTemplate)
}

45
internal/widget/iframe.go Normal file
View File

@@ -0,0 +1,45 @@
package widget
import (
"errors"
"fmt"
"html/template"
"net/url"
"github.com/glanceapp/glance/internal/assets"
)
type IFrame struct {
widgetBase `yaml:",inline"`
cachedHTML template.HTML `yaml:"-"`
Source string `yaml:"source"`
Height int `yaml:"height"`
}
func (widget *IFrame) Initialize() error {
widget.withTitle("IFrame").withError(nil)
if widget.Source == "" {
return errors.New("missing source for iframe")
}
_, err := url.Parse(widget.Source)
if err != nil {
return fmt.Errorf("invalid source for iframe: %v", err)
}
if widget.Height == 50 {
widget.Height = 300
} else if widget.Height < 50 {
widget.Height = 50
}
widget.cachedHTML = widget.render(widget, assets.IFrameTemplate)
return nil
}
func (widget *IFrame) Render() template.HTML {
return widget.cachedHTML
}

100
internal/widget/monitor.go Normal file
View File

@@ -0,0 +1,100 @@
package widget
import (
"context"
"fmt"
"html/template"
"net/http"
"strconv"
"time"
"github.com/glanceapp/glance/internal/assets"
"github.com/glanceapp/glance/internal/feed"
)
func statusCodeToText(status int) string {
if status == 200 {
return "OK"
}
if status == 404 {
return "Not Found"
}
if status == 403 {
return "Forbidden"
}
if status == 401 {
return "Unauthorized"
}
if status >= 400 {
return "Client Error"
}
if status >= 500 {
return "Server Error"
}
return strconv.Itoa(status)
}
func statusCodeToStyle(status int) string {
if status == 200 {
return "good"
}
return "bad"
}
type Monitor struct {
widgetBase `yaml:",inline"`
Sites []struct {
Title string `yaml:"title"`
Url string `yaml:"url"`
IconUrl string `yaml:"icon"`
Status *feed.SiteStatus `yaml:"-"`
StatusText string `yaml:"-"`
StatusStyle string `yaml:"-"`
} `yaml:"sites"`
}
func (widget *Monitor) Initialize() error {
widget.withTitle("Monitor").withCacheDuration(5 * time.Minute)
return nil
}
func (widget *Monitor) Update(ctx context.Context) {
requests := make([]*http.Request, len(widget.Sites))
for i := range widget.Sites {
request, err := http.NewRequest("HEAD", widget.Sites[i].Url, nil)
if err != nil {
message := fmt.Errorf("failed to create http request for %s: %s", widget.Sites[i].Url, err)
widget.withNotice(message)
continue
}
requests[i] = request
}
statuses, err := feed.FetchStatusesForRequests(requests)
if !widget.canContinueUpdateAfterHandlingErr(err) {
return
}
for i := range widget.Sites {
site := &widget.Sites[i]
status := &statuses[i]
site.Status = status
if !status.TimedOut {
site.StatusText = statusCodeToText(status.Code)
site.StatusStyle = statusCodeToStyle(status.Code)
}
}
}
func (widget *Monitor) Render() template.HTML {
return widget.render(widget, assets.MonitorTemplate)
}

66
internal/widget/reddit.go Normal file
View File

@@ -0,0 +1,66 @@
package widget
import (
"context"
"errors"
"html/template"
"time"
"github.com/glanceapp/glance/internal/assets"
"github.com/glanceapp/glance/internal/feed"
)
type Reddit struct {
widgetBase `yaml:",inline"`
Posts feed.ForumPosts `yaml:"-"`
Subreddit string `yaml:"subreddit"`
Style string `yaml:"style"`
Limit int `yaml:"limit"`
CollapseAfter int `yaml:"collapse-after"`
}
func (widget *Reddit) Initialize() error {
if widget.Subreddit == "" {
return errors.New("no subreddit specified")
}
if widget.Limit <= 0 {
widget.Limit = 15
}
if widget.CollapseAfter == 0 || widget.CollapseAfter < -1 {
widget.CollapseAfter = 5
}
widget.withTitle("/r/" + widget.Subreddit).withCacheDuration(30 * time.Minute)
return nil
}
func (widget *Reddit) Update(ctx context.Context) {
posts, err := feed.FetchSubredditPosts(widget.Subreddit)
if !widget.canContinueUpdateAfterHandlingErr(err) {
return
}
if len(posts) > widget.Limit {
posts = posts[:widget.Limit]
}
posts.SortByEngagement()
widget.Posts = posts
}
func (widget *Reddit) Render() template.HTML {
if widget.Style == "horizontal-cards" {
return widget.render(widget, assets.RedditCardsHorizontalTemplate)
}
if widget.Style == "vertical-cards" {
return widget.render(widget, assets.RedditCardsVerticalTemplate)
}
return widget.render(widget, assets.ForumPostsTemplate)
}

View File

@@ -0,0 +1,51 @@
package widget
import (
"context"
"html/template"
"time"
"github.com/glanceapp/glance/internal/assets"
"github.com/glanceapp/glance/internal/feed"
)
type Releases struct {
widgetBase `yaml:",inline"`
Releases feed.AppReleases `yaml:"-"`
Repositories []string `yaml:"repositories"`
Token OptionalEnvString `yaml:"token"`
Limit int `yaml:"limit"`
CollapseAfter int `yaml:"collapse-after"`
}
func (widget *Releases) Initialize() error {
widget.withTitle("Releases").withCacheDuration(2 * time.Hour)
if widget.Limit <= 0 {
widget.Limit = 10
}
if widget.CollapseAfter == 0 || widget.CollapseAfter < -1 {
widget.CollapseAfter = 5
}
return nil
}
func (widget *Releases) Update(ctx context.Context) {
releases, err := feed.FetchLatestReleasesFromGithub(widget.Repositories, string(widget.Token))
if !widget.canContinueUpdateAfterHandlingErr(err) {
return
}
if len(releases) > widget.Limit {
releases = releases[:widget.Limit]
}
widget.Releases = releases
}
func (widget *Releases) Render() template.HTML {
return widget.render(widget, assets.ReleasesTemplate)
}

55
internal/widget/rss.go Normal file
View File

@@ -0,0 +1,55 @@
package widget
import (
"context"
"html/template"
"time"
"github.com/glanceapp/glance/internal/assets"
"github.com/glanceapp/glance/internal/feed"
)
type RSS struct {
widgetBase `yaml:",inline"`
FeedRequests []feed.RSSFeedRequest `yaml:"feeds"`
Style string `yaml:"style"`
Items feed.RSSFeedItems `yaml:"-"`
Limit int `yaml:"limit"`
CollapseAfter int `yaml:"collapse-after"`
}
func (widget *RSS) Initialize() error {
widget.withTitle("RSS Feed").withCacheDuration(1 * time.Hour)
if widget.Limit <= 0 {
widget.Limit = 25
}
if widget.CollapseAfter == 0 || widget.CollapseAfter < -1 {
widget.CollapseAfter = 5
}
return nil
}
func (widget *RSS) Update(ctx context.Context) {
items, err := feed.GetItemsFromRSSFeeds(widget.FeedRequests)
if !widget.canContinueUpdateAfterHandlingErr(err) {
return
}
if len(items) > widget.Limit {
items = items[:widget.Limit]
}
widget.Items = items
}
func (widget *RSS) Render() template.HTML {
if widget.Style == "horizontal-cards" {
return widget.render(widget, assets.RSSCardsTemplate)
}
return widget.render(widget, assets.RSSListTemplate)
}

37
internal/widget/stocks.go Normal file
View File

@@ -0,0 +1,37 @@
package widget
import (
"context"
"html/template"
"time"
"github.com/glanceapp/glance/internal/assets"
"github.com/glanceapp/glance/internal/feed"
)
type Stocks struct {
widgetBase `yaml:",inline"`
Stocks feed.Stocks `yaml:"-"`
Tickers []feed.StockRequest `yaml:"stocks"`
}
func (widget *Stocks) Initialize() error {
widget.withTitle("Stocks").withCacheDuration(time.Hour)
return nil
}
func (widget *Stocks) Update(ctx context.Context) {
stocks, err := feed.FetchStocksDataFromYahoo(widget.Tickers)
if !widget.canContinueUpdateAfterHandlingErr(err) {
return
}
stocks.SortByAbsChange()
widget.Stocks = stocks
}
func (widget *Stocks) Render() template.HTML {
return widget.render(widget, assets.StocksTemplate)
}

View File

@@ -0,0 +1,42 @@
package widget
import (
"context"
"html/template"
"time"
"github.com/glanceapp/glance/internal/assets"
"github.com/glanceapp/glance/internal/feed"
)
type TwitchChannels struct {
widgetBase `yaml:",inline"`
ChannelsRequest []string `yaml:"channels"`
Channels []feed.TwitchChannel `yaml:"-"`
CollapseAfter int `yaml:"collapse-after"`
}
func (widget *TwitchChannels) Initialize() error {
widget.withTitle("Twitch Channels").withCacheDuration(time.Minute * 10)
if widget.CollapseAfter == 0 || widget.CollapseAfter < -1 {
widget.CollapseAfter = 5
}
return nil
}
func (widget *TwitchChannels) Update(ctx context.Context) {
channels, err := feed.FetchChannelsFromTwitch(widget.ChannelsRequest)
if !widget.canContinueUpdateAfterHandlingErr(err) {
return
}
channels.SortByViewers()
widget.Channels = channels
}
func (widget *TwitchChannels) Render() template.HTML {
return widget.render(widget, assets.TwitchChannelsTemplate)
}

View File

@@ -0,0 +1,46 @@
package widget
import (
"context"
"html/template"
"time"
"github.com/glanceapp/glance/internal/assets"
"github.com/glanceapp/glance/internal/feed"
)
type TwitchGames struct {
widgetBase `yaml:",inline"`
Categories []feed.TwitchCategory `yaml:"-"`
Exclude []string `yaml:"exclude"`
Limit int `yaml:"limit"`
CollapseAfter int `yaml:"collapse-after"`
}
func (widget *TwitchGames) Initialize() error {
widget.withTitle("Top games on Twitch").withCacheDuration(time.Minute * 10)
if widget.Limit <= 0 {
widget.Limit = 10
}
if widget.CollapseAfter == 0 || widget.CollapseAfter < -1 {
widget.CollapseAfter = 5
}
return nil
}
func (widget *TwitchGames) Update(ctx context.Context) {
categories, err := feed.FetchTopGamesFromTwitch(widget.Exclude, widget.Limit)
if !widget.canContinueUpdateAfterHandlingErr(err) {
return
}
widget.Categories = categories
}
func (widget *TwitchGames) Render() template.HTML {
return widget.render(widget, assets.TwitchGamesListTemplate)
}

45
internal/widget/videos.go Normal file
View File

@@ -0,0 +1,45 @@
package widget
import (
"context"
"html/template"
"time"
"github.com/glanceapp/glance/internal/assets"
"github.com/glanceapp/glance/internal/feed"
)
type Videos struct {
widgetBase `yaml:",inline"`
Videos feed.Videos `yaml:"-"`
Channels []string `yaml:"channels"`
Limit int `yaml:"limit"`
}
func (widget *Videos) Initialize() error {
widget.withTitle("Videos").withCacheDuration(time.Hour)
if widget.Limit <= 0 {
widget.Limit = 25
}
return nil
}
func (widget *Videos) Update(ctx context.Context) {
videos, err := feed.FetchYoutubeChannelUploads(widget.Channels)
if !widget.canContinueUpdateAfterHandlingErr(err) {
return
}
if len(videos) > widget.Limit {
videos = videos[:widget.Limit]
}
widget.Videos = videos
}
func (widget *Videos) Render() template.HTML {
return widget.render(widget, assets.VideosTemplate)
}

View File

@@ -0,0 +1,50 @@
package widget
import (
"context"
"fmt"
"html/template"
"github.com/glanceapp/glance/internal/assets"
"github.com/glanceapp/glance/internal/feed"
)
type Weather struct {
widgetBase `yaml:",inline"`
Location string `yaml:"location"`
HideLocation bool `yaml:"hide-location"`
Place *feed.PlaceJson `yaml:"-"`
Weather *feed.Weather `yaml:"-"`
TimeLabels [12]string `yaml:"-"`
}
var timeLabels = [12]string{"2am", "4am", "6am", "8am", "10am", "12pm", "2pm", "4pm", "6pm", "8pm", "10pm", "12am"}
func (widget *Weather) Initialize() error {
widget.withTitle("Weather").withCacheOnTheHour()
widget.TimeLabels = timeLabels
place, err := feed.FetchPlaceFromName(widget.Location)
if err != nil {
return fmt.Errorf("failed fetching data for %s: %v", widget.Location, err)
}
widget.Place = place
return nil
}
func (widget *Weather) Update(ctx context.Context) {
weather, err := feed.FetchWeatherForPlace(widget.Place)
if !widget.canContinueUpdateAfterHandlingErr(err) {
return
}
widget.Weather = weather
}
func (widget *Weather) Render() template.HTML {
return widget.render(widget, assets.WeatherTemplate)
}

282
internal/widget/widget.go Normal file
View File

@@ -0,0 +1,282 @@
package widget
import (
"bytes"
"context"
"errors"
"fmt"
"html/template"
"log/slog"
"math"
"time"
"github.com/glanceapp/glance/internal/feed"
"gopkg.in/yaml.v3"
)
func New(widgetType string) (Widget, error) {
switch widgetType {
case "calendar":
return &Calendar{}, nil
case "weather":
return &Weather{}, nil
case "bookmarks":
return &Bookmarks{}, nil
case "iframe":
return &IFrame{}, nil
case "hacker-news":
return &HackerNews{}, nil
case "releases":
return &Releases{}, nil
case "videos":
return &Videos{}, nil
case "stocks":
return &Stocks{}, nil
case "reddit":
return &Reddit{}, nil
case "rss":
return &RSS{}, nil
case "monitor":
return &Monitor{}, nil
case "twitch-top-games":
return &TwitchGames{}, nil
case "twitch-channels":
return &TwitchChannels{}, nil
default:
return nil, fmt.Errorf("unknown widget type: %s", widgetType)
}
}
type Widgets []Widget
func (w *Widgets) UnmarshalYAML(node *yaml.Node) error {
var nodes []yaml.Node
if err := node.Decode(&nodes); err != nil {
return err
}
for _, node := range nodes {
meta := struct {
Type string `yaml:"type"`
}{}
if err := node.Decode(&meta); err != nil {
return err
}
widget, err := New(meta.Type)
if err != nil {
return err
}
if err = node.Decode(widget); err != nil {
return err
}
if err = widget.Initialize(); err != nil {
return err
}
*w = append(*w, widget)
}
return nil
}
type Widget interface {
Initialize() error
RequiresUpdate(*time.Time) bool
Update(context.Context)
Render() template.HTML
GetType() string
}
type cacheType int
const (
cacheTypeInfinite cacheType = iota
cacheTypeDuration
cacheTypeOnTheHour
)
type widgetBase struct {
Type string `yaml:"type"`
Title string `yaml:"title"`
CustomCacheDuration DurationField `yaml:"cache"`
ContentAvailable bool `yaml:"-"`
Error error `yaml:"-"`
Notice error `yaml:"-"`
templateBuffer bytes.Buffer `yaml:"-"`
cacheDuration time.Duration `yaml:"-"`
cacheType cacheType `yaml:"-"`
nextUpdate time.Time `yaml:"-"`
updateRetriedTimes int `yaml:"-"`
}
func (w *widgetBase) RequiresUpdate(now *time.Time) bool {
if w.cacheType == cacheTypeInfinite {
return false
}
if w.nextUpdate.IsZero() {
return true
}
return now.After(w.nextUpdate)
}
func (w *widgetBase) Update(ctx context.Context) {
}
func (w *widgetBase) GetType() string {
return w.Type
}
func (w *widgetBase) render(data any, t *template.Template) template.HTML {
w.templateBuffer.Reset()
err := t.Execute(&w.templateBuffer, data)
if err != nil {
w.ContentAvailable = false
w.Error = err
slog.Error("failed to render template", "error", err)
// need to immediately re-render with the error,
// otherwise risk breaking the page since the widget
// will likely be partially rendered with tags not closed.
w.templateBuffer.Reset()
err2 := t.Execute(&w.templateBuffer, data)
if err2 != nil {
slog.Error("failed to render error within widget", "error", err2, "initial_error", err)
w.templateBuffer.Reset()
// TODO: add some kind of a generic widget error template when the widget
// failed to render, and we also failed to re-render the widget with the error
}
}
return template.HTML(w.templateBuffer.String())
}
func (w *widgetBase) withTitle(title string) *widgetBase {
if w.Title == "" {
w.Title = title
}
return w
}
func (w *widgetBase) withCacheDuration(duration time.Duration) *widgetBase {
w.cacheType = cacheTypeDuration
if duration == -1 || w.CustomCacheDuration == 0 {
w.cacheDuration = duration
} else {
w.cacheDuration = time.Duration(w.CustomCacheDuration)
}
return w
}
func (w *widgetBase) withCacheOnTheHour() *widgetBase {
w.cacheType = cacheTypeOnTheHour
return w
}
func (w *widgetBase) withNotice(err error) *widgetBase {
w.Notice = err
return w
}
func (w *widgetBase) withError(err error) *widgetBase {
if err == nil && !w.ContentAvailable {
w.ContentAvailable = true
}
w.Error = err
return w
}
func (w *widgetBase) canContinueUpdateAfterHandlingErr(err error) bool {
// TODO: needs covering more edge cases.
// if there's partial content and we update early there's a chance
// the early update returns even less content than the initial update.
// need some kind of mechanism that tells us whether we should update early
// or not depending on the number of things that failed during the initial
// and subsequent update and how they failed - ie whether it was server
// error (like gateway timeout, do retry early) or client error (like
// hitting a rate limit, don't retry early). will require reworking a
// good amount of code in the feed package and probably having a custom
// error type that holds more information because screw wrapping errors.
// alternatively have a resource cache and only refetch the failed resources,
// then rebuild the widget.
if err != nil {
w.scheduleEarlyUpdate()
if !errors.Is(err, feed.ErrPartialContent) {
w.withError(err)
w.withNotice(nil)
return false
}
w.withError(nil)
w.withNotice(err)
return true
}
w.withNotice(nil)
w.withError(nil)
w.scheduleNextUpdate()
return true
}
func (w *widgetBase) getNextUpdateTime() time.Time {
now := time.Now()
if w.cacheType == cacheTypeDuration {
return now.Add(w.cacheDuration)
}
if w.cacheType == cacheTypeOnTheHour {
return now.Add(time.Duration(
((60-now.Minute())*60)-now.Second(),
) * time.Second)
}
return time.Time{}
}
func (w *widgetBase) scheduleNextUpdate() *widgetBase {
w.nextUpdate = w.getNextUpdateTime()
w.updateRetriedTimes = 0
return w
}
func (w *widgetBase) scheduleEarlyUpdate() *widgetBase {
w.updateRetriedTimes++
if w.updateRetriedTimes > 5 {
w.updateRetriedTimes = 5
}
nextEarlyUpdate := time.Now().Add(time.Duration(math.Pow(float64(w.updateRetriedTimes), 2)) * time.Minute)
nextUsualUpdate := w.getNextUpdateTime()
if nextEarlyUpdate.After(nextUsualUpdate) {
w.nextUpdate = nextUsualUpdate
} else {
w.nextUpdate = nextEarlyUpdate
}
return w
}