diff --git a/internal/assets/templates.go b/internal/assets/templates.go index fe82ed5..3c9f691 100644 --- a/internal/assets/templates.go +++ b/internal/assets/templates.go @@ -22,6 +22,7 @@ var ( RedditCardsHorizontalTemplate = compileTemplate("reddit-horizontal-cards.html", "widget-base.html") RedditCardsVerticalTemplate = compileTemplate("reddit-vertical-cards.html", "widget-base.html") ReleasesTemplate = compileTemplate("releases.html", "widget-base.html") + ChangesTemplate = compileTemplate("changes.html", "widget-base.html") VideosTemplate = compileTemplate("videos.html", "widget-base.html") StocksTemplate = compileTemplate("stocks.html", "widget-base.html") RSSListTemplate = compileTemplate("rss-list.html", "widget-base.html") diff --git a/internal/assets/templates/changes.html b/internal/assets/templates/changes.html new file mode 100644 index 0000000..d8eee38 --- /dev/null +++ b/internal/assets/templates/changes.html @@ -0,0 +1,17 @@ +{{ template "widget-base.html" . }} + +{{ define "widget-content" }} + +{{ if gt (len .ChangeDetections) $.CollapseAfter }} + +{{ end }} +{{ end }} diff --git a/internal/feed/changedetection.go b/internal/feed/changedetection.go new file mode 100644 index 0000000..6bfd119 --- /dev/null +++ b/internal/feed/changedetection.go @@ -0,0 +1,79 @@ +package feed + +import ( + "fmt" + "log/slog" + "net/http" + "time" +) + +type changeDetectionResponseJson struct { + Name string `json:"title"` + Url string `json:"url"` + LastChanged int `json:"last_changed"` +} + + +func parseLastChangeTime(t int) time.Time { + parsedTime := time.Unix(int64(t), 0) + return parsedTime +} + + +func FetchLatestDetectedChanges(watches []string, token string) (ChangeWatches, error) { + changeWatches := make(ChangeWatches, 0, len(watches)) + + if len(watches) == 0 { + return changeWatches, nil + } + + requests := make([]*http.Request, len(watches)) + + for i, repository := range watches { + request, _ := http.NewRequest("GET", fmt.Sprintf("https://changedetection.knhash.in/api/v1/watch/%s", repository), nil) + + if token != "" { + request.Header.Add("x-api-key", token) + } + + requests[i] = request + } + + task := decodeJsonFromRequestTask[changeDetectionResponseJson](defaultClient) + job := newJob(task, requests).withWorkers(15) + responses, errs, err := workerPoolDo(job) + + if err != nil { + return nil, err + } + + var failed int + + for i := range responses { + if errs[i] != nil { + failed++ + slog.Error("Failed to fetch or parse change detections", "error", errs[i], "url", requests[i].URL) + continue + } + + watch := responses[i] + + changeWatches = append(changeWatches, ChangeWatch{ + Name: watch.Name, + Url: watch.Url, + LastChanged: parseLastChangeTime(watch.LastChanged), + }) + } + + if len(changeWatches) == 0 { + return nil, ErrNoContent + } + + changeWatches.SortByNewest() + + if failed > 0 { + return changeWatches, fmt.Errorf("%w: could not get %d watches", ErrPartialContent, failed) + } + + return changeWatches, nil +} diff --git a/internal/feed/primitives.go b/internal/feed/primitives.go index 99d6763..70e54a1 100644 --- a/internal/feed/primitives.go +++ b/internal/feed/primitives.go @@ -48,6 +48,14 @@ type AppRelease struct { type AppReleases []AppRelease +type ChangeWatch struct { + Name string + Url string + LastChanged time.Time +} + +type ChangeWatches []ChangeWatch + type Video struct { ThumbnailUrl string Title string @@ -200,6 +208,14 @@ func (r AppReleases) SortByNewest() AppReleases { return r } +func (r ChangeWatches) SortByNewest() ChangeWatches { + sort.Slice(r, func(i, j int) bool { + return r[i].LastChanged.After(r[j].LastChanged) + }) + + return r +} + func (v Videos) SortByNewest() Videos { sort.Slice(v, func(i, j int) bool { return v[i].TimePosted.After(v[j].TimePosted) diff --git a/internal/widget/changedetection.go b/internal/widget/changedetection.go new file mode 100644 index 0000000..589147e --- /dev/null +++ b/internal/widget/changedetection.go @@ -0,0 +1,51 @@ +package widget + +import ( + "context" + "html/template" + "time" + + "github.com/glanceapp/glance/internal/assets" + "github.com/glanceapp/glance/internal/feed" +) + +type ChangeDetections struct { + widgetBase `yaml:",inline"` + ChangeDetections feed.ChangeWatches `yaml:"-"` + Watches []string `yaml:"watches"` + Token OptionalEnvString `yaml:"token"` + Limit int `yaml:"limit"` + CollapseAfter int `yaml:"collapse-after"` +} + +func (widget *ChangeDetections) Initialize() error { + widget.withTitle("Changes").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 *ChangeDetections) Update(ctx context.Context) { + watches, err := feed.FetchLatestDetectedChanges(widget.Watches, string(widget.Token)) + + if !widget.canContinueUpdateAfterHandlingErr(err) { + return + } + + if len(watches) > widget.Limit { + watches = watches[:widget.Limit] + } + + widget.ChangeDetections = watches +} + +func (widget *ChangeDetections) Render() template.HTML { + return widget.render(widget, assets.ChangesTemplate) +} diff --git a/internal/widget/widget.go b/internal/widget/widget.go index 367d822..e7ec293 100644 --- a/internal/widget/widget.go +++ b/internal/widget/widget.go @@ -43,8 +43,10 @@ func New(widgetType string) (Widget, error) { return &TwitchGames{}, nil case "twitch-channels": return &TwitchChannels{}, nil + case "changes": + return &ChangeDetections{}, nil default: - return nil, fmt.Errorf("unknown widget type: %s", widgetType) + return nil, fmt.Errorf("unknown widget type: %s found", widgetType) } }