mirror of
https://github.com/glanceapp/glance.git
synced 2025-06-22 02:41:23 +02:00
Update change detection
This commit is contained in:
parent
6cad5a8efb
commit
00a93e466d
@ -22,7 +22,7 @@ var (
|
|||||||
RedditCardsHorizontalTemplate = compileTemplate("reddit-horizontal-cards.html", "widget-base.html")
|
RedditCardsHorizontalTemplate = compileTemplate("reddit-horizontal-cards.html", "widget-base.html")
|
||||||
RedditCardsVerticalTemplate = compileTemplate("reddit-vertical-cards.html", "widget-base.html")
|
RedditCardsVerticalTemplate = compileTemplate("reddit-vertical-cards.html", "widget-base.html")
|
||||||
ReleasesTemplate = compileTemplate("releases.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")
|
VideosTemplate = compileTemplate("videos.html", "widget-base.html", "video-card-contents.html")
|
||||||
VideosGridTemplate = compileTemplate("videos-grid.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")
|
StocksTemplate = compileTemplate("stocks.html", "widget-base.html")
|
||||||
|
17
internal/assets/templates/change-detection.html
Normal file
17
internal/assets/templates/change-detection.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{{ template "widget-base.html" . }}
|
||||||
|
|
||||||
|
{{ define "widget-content" }}
|
||||||
|
<ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
|
||||||
|
{{ range .ChangeDetections }}
|
||||||
|
<li>
|
||||||
|
<a class="size-h4 block text-truncate color-highlight" href="{{ .URL }}" target="_blank" rel="noreferrer">{{ .Title }}</a>
|
||||||
|
<ul class="list-horizontal-text">
|
||||||
|
<li {{ dynamicRelativeTimeAttrs .LastChanged }}></li>
|
||||||
|
<li class="shrink min-width-0"><a class="visited-indicator" href="{{ .DiffURL }}" target="_blank" rel="noreferrer">diff:{{ .PreviousHash }}</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{{ else }}
|
||||||
|
<li>No watches configured</li>
|
||||||
|
{{ end}}
|
||||||
|
</ul>
|
||||||
|
{{ end }}
|
@ -1,18 +0,0 @@
|
|||||||
{{ template "widget-base.html" . }}
|
|
||||||
|
|
||||||
{{ define "widget-content" }}
|
|
||||||
<ul class="list list-gap-14 list-collapsible">
|
|
||||||
{{ range $i, $watch := .ChangeDetections }}
|
|
||||||
<li {{ if shouldCollapse $i $.CollapseAfter }}class="list-collapsible-item" style="--animation-delay: {{ itemAnimationDelay $i $.CollapseAfter }};"{{ end }}>
|
|
||||||
<a class="size-h4 block text-truncate color-primary-if-not-visited" href="{{ $watch.URL }}" target="_blank" rel="noreferrer">{{ .Name }}</a>
|
|
||||||
<ul class="list-horizontal-text">
|
|
||||||
<li title="{{ $watch.LastChanged | formatTime }}" {{ dynamicRelativeTimeAttrs $watch.LastChanged }}>{{ $watch.LastChanged | relativeTime }}</li>
|
|
||||||
<li class="shrink min-width-0"><a class="visited-indicator text-truncate block" href="{{ $watch.DiffURL }}" target="_blank" rel="noreferrer">diff: {{ $watch.DiffDisplay | }}</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
{{ end }}
|
|
||||||
</ul>
|
|
||||||
{{ if gt (len .ChangeDetections) $.CollapseAfter }}
|
|
||||||
<label class="list-collapsible-label"><input type="checkbox" autocomplete="off" class="list-collapsible-input"></label>
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
@ -4,37 +4,70 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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 {
|
type changeDetectionResponseJson struct {
|
||||||
Name string `json:"title"`
|
Title string `json:"title"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
LastChanged int `json:"last_changed"`
|
LastChanged int64 `json:"last_changed"`
|
||||||
UUID string `json:"uuid"`
|
DateCreated int64 `json:"date_created"`
|
||||||
|
PreviousHash string `json:"previous_md5"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseLastChangeTime(t int) time.Time {
|
func FetchWatchUUIDsFromChangeDetection(instanceURL string, token string) ([]string, error) {
|
||||||
parsedTime := time.Unix(int64(t), 0)
|
request, _ := http.NewRequest("GET", fmt.Sprintf("%s/api/v1/watch", instanceURL), nil)
|
||||||
return parsedTime
|
|
||||||
|
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) {
|
func FetchWatchesFromChangeDetection(instanceURL string, requestedWatchIDs []string, token string) (ChangeDetectionWatches, error) {
|
||||||
changeWatches := make(ChangeWatches, 0, len(watches))
|
watches := make(ChangeDetectionWatches, 0, len(requestedWatchIDs))
|
||||||
|
|
||||||
if request_url == "" {
|
if len(requestedWatchIDs) == 0 {
|
||||||
request_url = "https://www.changedetection.io"
|
return watches, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(watches) == 0 {
|
requests := make([]*http.Request, len(requestedWatchIDs))
|
||||||
return changeWatches, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
requests := make([]*http.Request, len(watches))
|
for i, repository := range requestedWatchIDs {
|
||||||
|
request, _ := http.NewRequest("GET", fmt.Sprintf("%s/api/v1/watch/%s", instanceURL, repository), nil)
|
||||||
for i, repository := range watches {
|
|
||||||
request, _ := http.NewRequest("GET", fmt.Sprintf("%s/api/v1/watch/%s", request_url, repository), nil)
|
|
||||||
|
|
||||||
if token != "" {
|
if token != "" {
|
||||||
request.Header.Add("x-api-key", 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 {
|
for i := range responses {
|
||||||
if errs[i] != nil {
|
if errs[i] != nil {
|
||||||
failed++
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
watch := responses[i]
|
watchJson := responses[i]
|
||||||
|
|
||||||
changeWatches = append(changeWatches, ChangeWatch{
|
watch := ChangeDetectionWatch{
|
||||||
Name: watch.Name,
|
URL: watchJson.URL,
|
||||||
URL: watch.URL,
|
DiffURL: fmt.Sprintf("%s/diff/%s?from_version=%d", instanceURL, requestedWatchIDs[i], watchJson.LastChanged-1),
|
||||||
LastChanged: parseLastChangeTime(watch.LastChanged),
|
|
||||||
DiffURL: request_url + "/diff/" + watch.UUID,
|
|
||||||
DiffDisplay: strings.Split(watch.UUID, "-")[len(strings.Split(watch.UUID, "-"))-1],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(changeWatches) == 0 {
|
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(watches) == 0 {
|
||||||
return nil, ErrNoContent
|
return nil, ErrNoContent
|
||||||
}
|
}
|
||||||
|
|
||||||
changeWatches.SortByNewest()
|
watches.SortByNewest()
|
||||||
|
|
||||||
if failed > 0 {
|
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
|
||||||
}
|
}
|
||||||
|
@ -48,16 +48,6 @@ type AppRelease struct {
|
|||||||
|
|
||||||
type AppReleases []AppRelease
|
type AppReleases []AppRelease
|
||||||
|
|
||||||
type ChangeWatch struct {
|
|
||||||
Name string
|
|
||||||
URL string
|
|
||||||
LastChanged time.Time
|
|
||||||
DiffURL string
|
|
||||||
DiffDisplay string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChangeWatches []ChangeWatch
|
|
||||||
|
|
||||||
type Video struct {
|
type Video struct {
|
||||||
ThumbnailUrl string
|
ThumbnailUrl string
|
||||||
Title string
|
Title string
|
||||||
@ -212,14 +202,6 @@ func (r AppReleases) SortByNewest() AppReleases {
|
|||||||
return r
|
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 {
|
func (v Videos) SortByNewest() Videos {
|
||||||
sort.Slice(v, func(i, j int) bool {
|
sort.Slice(v, func(i, j int) bool {
|
||||||
return v[i].TimePosted.After(v[j].TimePosted)
|
return v[i].TimePosted.After(v[j].TimePosted)
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@ -77,3 +78,9 @@ func maybeCopySliceWithoutZeroValues[T int | float64](values []T) []T {
|
|||||||
|
|
||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var urlSchemePattern = regexp.MustCompile(`^[a-z]+:\/\/`)
|
||||||
|
|
||||||
|
func stripURLScheme(url string) string {
|
||||||
|
return urlSchemePattern.ReplaceAllString(url, "")
|
||||||
|
}
|
||||||
|
@ -9,18 +9,18 @@ import (
|
|||||||
"github.com/glanceapp/glance/internal/feed"
|
"github.com/glanceapp/glance/internal/feed"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChangeDetections struct {
|
type ChangeDetection struct {
|
||||||
widgetBase `yaml:",inline"`
|
widgetBase `yaml:",inline"`
|
||||||
ChangeDetections feed.ChangeWatches `yaml:"-"`
|
ChangeDetections feed.ChangeDetectionWatches `yaml:"-"`
|
||||||
RequestURL string `yaml:"request_url"`
|
WatchUUIDs []string `yaml:"watches"`
|
||||||
Watches []string `yaml:"watches"`
|
InstanceURL string `yaml:"instance-url"`
|
||||||
Token OptionalEnvString `yaml:"token"`
|
Token OptionalEnvString `yaml:"token"`
|
||||||
Limit int `yaml:"limit"`
|
Limit int `yaml:"limit"`
|
||||||
CollapseAfter int `yaml:"collapse-after"`
|
CollapseAfter int `yaml:"collapse-after"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (widget *ChangeDetections) Initialize() error {
|
func (widget *ChangeDetection) Initialize() error {
|
||||||
widget.withTitle("Changes").withCacheDuration(2 * time.Hour)
|
widget.withTitle("Change Detection").withCacheDuration(1 * time.Hour)
|
||||||
|
|
||||||
if widget.Limit <= 0 {
|
if widget.Limit <= 0 {
|
||||||
widget.Limit = 10
|
widget.Limit = 10
|
||||||
@ -30,11 +30,25 @@ func (widget *ChangeDetections) Initialize() error {
|
|||||||
widget.CollapseAfter = 5
|
widget.CollapseAfter = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if widget.InstanceURL == "" {
|
||||||
|
widget.InstanceURL = "https://www.changedetection.io"
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (widget *ChangeDetections) Update(ctx context.Context) {
|
func (widget *ChangeDetection) Update(ctx context.Context) {
|
||||||
watches, err := feed.FetchLatestDetectedChanges(widget.RequestURL, widget.Watches, string(widget.Token))
|
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) {
|
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
||||||
return
|
return
|
||||||
@ -47,6 +61,6 @@ func (widget *ChangeDetections) Update(ctx context.Context) {
|
|||||||
widget.ChangeDetections = watches
|
widget.ChangeDetections = watches
|
||||||
}
|
}
|
||||||
|
|
||||||
func (widget *ChangeDetections) Render() template.HTML {
|
func (widget *ChangeDetection) Render() template.HTML {
|
||||||
return widget.render(widget, assets.ChangesTemplate)
|
return widget.render(widget, assets.ChangeDetectionTemplate)
|
||||||
}
|
}
|
||||||
|
@ -43,12 +43,12 @@ func New(widgetType string) (Widget, error) {
|
|||||||
return &TwitchGames{}, nil
|
return &TwitchGames{}, nil
|
||||||
case "twitch-channels":
|
case "twitch-channels":
|
||||||
return &TwitchChannels{}, nil
|
return &TwitchChannels{}, nil
|
||||||
case "changes":
|
case "change-detection":
|
||||||
return &ChangeDetections{}, nil
|
return &ChangeDetection{}, nil
|
||||||
case "repository":
|
case "repository":
|
||||||
return &Repository{}, nil
|
return &Repository{}, nil
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown widget type: %s found", widgetType)
|
return nil, fmt.Errorf("unknown widget type: %s", widgetType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user