From 00a93e466df4d2cccbff97353bea2d63d3b9f075 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Thu, 30 May 2024 22:53:59 +0100 Subject: [PATCH] Update change detection --- internal/assets/templates.go | 2 +- .../assets/templates/change-detection.html | 17 +++ internal/assets/templates/changes.html | 18 --- internal/feed/changedetection.go | 116 +++++++++++++----- internal/feed/primitives.go | 18 --- internal/feed/utils.go | 7 ++ internal/widget/changedetection.go | 40 ++++-- internal/widget/widget.go | 6 +- 8 files changed, 140 insertions(+), 84 deletions(-) create mode 100644 internal/assets/templates/change-detection.html delete mode 100644 internal/assets/templates/changes.html 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" }} + +{{ 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) } }