diff --git a/internal/assets/templates.go b/internal/assets/templates.go
index 0abbb6e..68f8031 100644
--- a/internal/assets/templates.go
+++ b/internal/assets/templates.go
@@ -22,7 +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")
+ ChangeDetectionTemplate = compileTemplate("change-detection.html", "widget-base.html")
VideosTemplate = compileTemplate("videos.html", "widget-base.html", "video-card-contents.html")
VideosGridTemplate = compileTemplate("videos-grid.html", "widget-base.html", "video-card-contents.html")
StocksTemplate = compileTemplate("stocks.html", "widget-base.html")
diff --git a/internal/assets/templates/change-detection.html b/internal/assets/templates/change-detection.html
new file mode 100644
index 0000000..22b7a18
--- /dev/null
+++ b/internal/assets/templates/change-detection.html
@@ -0,0 +1,17 @@
+{{ template "widget-base.html" . }}
+
+{{ define "widget-content" }}
+
+ {{ range .ChangeDetections }}
+ -
+ {{ .Title }}
+
+
+ {{ else }}
+ - No watches configured
+ {{ end}}
+
+{{ end }}
diff --git a/internal/assets/templates/changes.html b/internal/assets/templates/changes.html
deleted file mode 100644
index 2b4c53a..0000000
--- a/internal/assets/templates/changes.html
+++ /dev/null
@@ -1,18 +0,0 @@
-{{ 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
index e1e3e45..793416d 100644
--- a/internal/feed/changedetection.go
+++ b/internal/feed/changedetection.go
@@ -4,37 +4,70 @@ import (
"fmt"
"log/slog"
"net/http"
+ "sort"
"strings"
"time"
)
+type ChangeDetectionWatch struct {
+ Title string
+ URL string
+ LastChanged time.Time
+ DiffURL string
+ PreviousHash string
+}
+
+type ChangeDetectionWatches []ChangeDetectionWatch
+
+func (r ChangeDetectionWatches) SortByNewest() ChangeDetectionWatches {
+ sort.Slice(r, func(i, j int) bool {
+ return r[i].LastChanged.After(r[j].LastChanged)
+ })
+
+ return r
+}
+
type changeDetectionResponseJson struct {
- Name string `json:"title"`
- URL string `json:"url"`
- LastChanged int `json:"last_changed"`
- UUID string `json:"uuid"`
+ Title string `json:"title"`
+ URL string `json:"url"`
+ LastChanged int64 `json:"last_changed"`
+ DateCreated int64 `json:"date_created"`
+ PreviousHash string `json:"previous_md5"`
}
-func parseLastChangeTime(t int) time.Time {
- parsedTime := time.Unix(int64(t), 0)
- return parsedTime
+func FetchWatchUUIDsFromChangeDetection(instanceURL string, token string) ([]string, error) {
+ request, _ := http.NewRequest("GET", fmt.Sprintf("%s/api/v1/watch", instanceURL), nil)
+
+ if token != "" {
+ request.Header.Add("x-api-key", token)
+ }
+
+ uuidsMap, err := decodeJsonFromRequest[map[string]struct{}](defaultClient, request)
+
+ if err != nil {
+ return nil, fmt.Errorf("could not fetch list of watch UUIDs: %v", err)
+ }
+
+ uuids := make([]string, 0, len(uuidsMap))
+
+ for uuid := range uuidsMap {
+ uuids = append(uuids, uuid)
+ }
+
+ return uuids, nil
}
-func FetchLatestDetectedChanges(request_url string, watches []string, token string) (ChangeWatches, error) {
- changeWatches := make(ChangeWatches, 0, len(watches))
+func FetchWatchesFromChangeDetection(instanceURL string, requestedWatchIDs []string, token string) (ChangeDetectionWatches, error) {
+ watches := make(ChangeDetectionWatches, 0, len(requestedWatchIDs))
- if request_url == "" {
- request_url = "https://www.changedetection.io"
+ if len(requestedWatchIDs) == 0 {
+ return watches, nil
}
- if len(watches) == 0 {
- return changeWatches, nil
- }
+ requests := make([]*http.Request, len(requestedWatchIDs))
- requests := make([]*http.Request, len(watches))
-
- for i, repository := range watches {
- request, _ := http.NewRequest("GET", fmt.Sprintf("%s/api/v1/watch/%s", request_url, repository), nil)
+ for i, repository := range requestedWatchIDs {
+ request, _ := http.NewRequest("GET", fmt.Sprintf("%s/api/v1/watch/%s", instanceURL, repository), nil)
if token != "" {
request.Header.Add("x-api-key", token)
@@ -56,30 +89,51 @@ func FetchLatestDetectedChanges(request_url string, watches []string, token stri
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)
+ slog.Error("Failed to fetch or parse change detection watch", "error", errs[i], "url", requests[i].URL)
continue
}
- watch := responses[i]
+ watchJson := responses[i]
- changeWatches = append(changeWatches, ChangeWatch{
- Name: watch.Name,
- URL: watch.URL,
- LastChanged: parseLastChangeTime(watch.LastChanged),
- DiffURL: request_url + "/diff/" + watch.UUID,
- DiffDisplay: strings.Split(watch.UUID, "-")[len(strings.Split(watch.UUID, "-"))-1],
- })
+ watch := ChangeDetectionWatch{
+ URL: watchJson.URL,
+ DiffURL: fmt.Sprintf("%s/diff/%s?from_version=%d", instanceURL, requestedWatchIDs[i], watchJson.LastChanged-1),
+ }
+
+ if watchJson.LastChanged == 0 {
+ watch.LastChanged = time.Unix(watchJson.DateCreated, 0)
+ } else {
+ watch.LastChanged = time.Unix(watchJson.LastChanged, 0)
+ }
+
+ if watchJson.Title != "" {
+ watch.Title = watchJson.Title
+ } else {
+ watch.Title = strings.TrimPrefix(strings.Trim(stripURLScheme(watchJson.URL), "/"), "www.")
+ }
+
+ if watchJson.PreviousHash != "" {
+ var hashLength = 8
+
+ if len(watchJson.PreviousHash) < hashLength {
+ hashLength = len(watchJson.PreviousHash)
+ }
+
+ watch.PreviousHash = watchJson.PreviousHash[0:hashLength]
+ }
+
+ watches = append(watches, watch)
}
- if len(changeWatches) == 0 {
+ if len(watches) == 0 {
return nil, ErrNoContent
}
- changeWatches.SortByNewest()
+ watches.SortByNewest()
if failed > 0 {
- return changeWatches, fmt.Errorf("%w: could not get %d watches", ErrPartialContent, failed)
+ return watches, fmt.Errorf("%w: could not get %d watches", ErrPartialContent, failed)
}
- return changeWatches, nil
+ return watches, nil
}
diff --git a/internal/feed/primitives.go b/internal/feed/primitives.go
index 139c02e..7982360 100644
--- a/internal/feed/primitives.go
+++ b/internal/feed/primitives.go
@@ -48,16 +48,6 @@ type AppRelease struct {
type AppReleases []AppRelease
-type ChangeWatch struct {
- Name string
- URL string
- LastChanged time.Time
- DiffURL string
- DiffDisplay string
-}
-
-type ChangeWatches []ChangeWatch
-
type Video struct {
ThumbnailUrl string
Title string
@@ -212,14 +202,6 @@ 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/feed/utils.go b/internal/feed/utils.go
index dcf044a..1409cee 100644
--- a/internal/feed/utils.go
+++ b/internal/feed/utils.go
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/url"
+ "regexp"
"slices"
"strings"
)
@@ -77,3 +78,9 @@ func maybeCopySliceWithoutZeroValues[T int | float64](values []T) []T {
return values
}
+
+var urlSchemePattern = regexp.MustCompile(`^[a-z]+:\/\/`)
+
+func stripURLScheme(url string) string {
+ return urlSchemePattern.ReplaceAllString(url, "")
+}
diff --git a/internal/widget/changedetection.go b/internal/widget/changedetection.go
index a1a65d8..26c080a 100644
--- a/internal/widget/changedetection.go
+++ b/internal/widget/changedetection.go
@@ -9,18 +9,18 @@ import (
"github.com/glanceapp/glance/internal/feed"
)
-type ChangeDetections struct {
+type ChangeDetection struct {
widgetBase `yaml:",inline"`
- ChangeDetections feed.ChangeWatches `yaml:"-"`
- RequestURL string `yaml:"request_url"`
- Watches []string `yaml:"watches"`
- Token OptionalEnvString `yaml:"token"`
- Limit int `yaml:"limit"`
- CollapseAfter int `yaml:"collapse-after"`
+ ChangeDetections feed.ChangeDetectionWatches `yaml:"-"`
+ WatchUUIDs []string `yaml:"watches"`
+ InstanceURL string `yaml:"instance-url"`
+ 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)
+func (widget *ChangeDetection) Initialize() error {
+ widget.withTitle("Change Detection").withCacheDuration(1 * time.Hour)
if widget.Limit <= 0 {
widget.Limit = 10
@@ -30,11 +30,25 @@ func (widget *ChangeDetections) Initialize() error {
widget.CollapseAfter = 5
}
+ if widget.InstanceURL == "" {
+ widget.InstanceURL = "https://www.changedetection.io"
+ }
+
return nil
}
-func (widget *ChangeDetections) Update(ctx context.Context) {
- watches, err := feed.FetchLatestDetectedChanges(widget.RequestURL, widget.Watches, string(widget.Token))
+func (widget *ChangeDetection) Update(ctx context.Context) {
+ if len(widget.WatchUUIDs) == 0 {
+ uuids, err := feed.FetchWatchUUIDsFromChangeDetection(widget.InstanceURL, string(widget.Token))
+
+ if !widget.canContinueUpdateAfterHandlingErr(err) {
+ return
+ }
+
+ widget.WatchUUIDs = uuids
+ }
+
+ watches, err := feed.FetchWatchesFromChangeDetection(widget.InstanceURL, widget.WatchUUIDs, string(widget.Token))
if !widget.canContinueUpdateAfterHandlingErr(err) {
return
@@ -47,6 +61,6 @@ func (widget *ChangeDetections) Update(ctx context.Context) {
widget.ChangeDetections = watches
}
-func (widget *ChangeDetections) Render() template.HTML {
- return widget.render(widget, assets.ChangesTemplate)
+func (widget *ChangeDetection) Render() template.HTML {
+ return widget.render(widget, assets.ChangeDetectionTemplate)
}
diff --git a/internal/widget/widget.go b/internal/widget/widget.go
index 39fa917..2158dd4 100644
--- a/internal/widget/widget.go
+++ b/internal/widget/widget.go
@@ -43,12 +43,12 @@ func New(widgetType string) (Widget, error) {
return &TwitchGames{}, nil
case "twitch-channels":
return &TwitchChannels{}, nil
- case "changes":
- return &ChangeDetections{}, nil
+ case "change-detection":
+ return &ChangeDetection{}, nil
case "repository":
return &Repository{}, nil
default:
- return nil, fmt.Errorf("unknown widget type: %s found", widgetType)
+ return nil, fmt.Errorf("unknown widget type: %s", widgetType)
}
}