diff --git a/internal/glance/templates/releases.html b/internal/glance/templates/releases.html index 3643524bb..5b6e002b5 100644 --- a/internal/glance/templates/releases.html +++ b/internal/glance/templates/releases.html @@ -16,6 +16,9 @@ {{ if gt .Downvotes 3 }}
  • {{ .Downvotes | formatNumber }} ⚠
  • {{ end }} + {{ if .CurrentVersion }} +
  • ({{ .CurrentVersion }})
  • + {{ end }} {{ end }} diff --git a/internal/glance/widget-releases.go b/internal/glance/widget-releases.go index de56bc51e..fbd59421c 100644 --- a/internal/glance/widget-releases.go +++ b/internal/glance/widget-releases.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "html/template" + "io" "log/slog" "net/http" "net/url" @@ -12,6 +13,7 @@ import ( "strings" "time" + "github.com/tidwall/gjson" "gopkg.in/yaml.v3" ) @@ -84,13 +86,14 @@ const ( ) type appRelease struct { - Source releaseSource - SourceIconURL string - Name string - Version string - NotesUrl string - TimeReleased time.Time - Downvotes int + Source releaseSource + SourceIconURL string + Name string + Version string + CurrentVersion string + NotesUrl string + TimeReleased time.Time + Downvotes int } type appReleaseList []appRelease @@ -106,7 +109,8 @@ func (r appReleaseList) sortByNewest() appReleaseList { type releaseRequest struct { IncludePreleases bool `yaml:"include-prereleases"` Repository string `yaml:"repository"` - + VersionEndpoint string `yaml:"version-endpoint"` + VersionPath string `yaml:"version-path"` source releaseSource token *string } @@ -188,18 +192,31 @@ func fetchLatestReleases(requests []*releaseRequest) (appReleaseList, error) { } func fetchLatestReleaseTask(request *releaseRequest) (*appRelease, error) { + var appRelease *appRelease + var err error + switch request.source { case releaseSourceCodeberg: - return fetchLatestCodebergRelease(request) + appRelease, err = fetchLatestCodebergRelease(request) case releaseSourceGithub: - return fetchLatestGithubRelease(request) + appRelease, err = fetchLatestGithubRelease(request) case releaseSourceGitlab: - return fetchLatestGitLabRelease(request) + appRelease, err = fetchLatestGitLabRelease(request) case releaseSourceDockerHub: - return fetchLatestDockerHubRelease(request) + appRelease, err = fetchLatestDockerHubRelease(request) + default: + return nil, errors.New("unsupported source") } - return nil, errors.New("unsupported source") + if err != nil { + return nil, err + } + + if request.VersionEndpoint != "" && request.VersionPath != "" { + return fetchCurrentVersion(request, appRelease) + } + + return appRelease, nil } type githubReleaseResponseJson struct { @@ -211,6 +228,51 @@ type githubReleaseResponseJson struct { } `json:"reactions"` } +func fetchCurrentVersion(request *releaseRequest, appRelease *appRelease) (*appRelease, error) { + httpRequest, err := http.NewRequest("GET", request.VersionEndpoint, nil) + if err != nil { + return nil, err + } + + resp, err := defaultHTTPClient.Do(httpRequest) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + body := strings.TrimSpace(string(bodyBytes)) + + if body != "" && !gjson.Valid(body) { + if 200 <= resp.StatusCode && resp.StatusCode < 300 { + truncatedBody, isTruncated := limitStringLength(body, 100) + if isTruncated { + truncatedBody += "... " + } + + slog.Error("Invalid response JSON in custom API widget", "url", httpRequest.URL.String(), "body", truncatedBody) + return nil, errors.New("invalid response JSON") + } + + return nil, fmt.Errorf("%d %s", resp.StatusCode, http.StatusText(resp.StatusCode)) + } + + version := gjson.Get(body, request.VersionPath).String() + + if version == "" { + return nil, errors.New("current version not found in response") + } + + appRelease.CurrentVersion = version + return appRelease, nil + +} + func fetchLatestGithubRelease(request *releaseRequest) (*appRelease, error) { var requestURL string if !request.IncludePreleases { diff --git a/internal/glance/widget-rss.go b/internal/glance/widget-rss.go index fe17b2fbd..90382e32f 100644 --- a/internal/glance/widget-rss.go +++ b/internal/glance/widget-rss.go @@ -308,7 +308,7 @@ func (widget *rssWidget) fetchItemsFromFeedTask(request rssFeedRequest) ([]rssFe } else { rssItem.ChannelName = feed.Title } - + if item.Image != nil { rssItem.ImageURL = item.Image.URL } else if url := findThumbnailInItemExtensions(item); url != "" { @@ -344,13 +344,31 @@ func (widget *rssWidget) fetchItemsFromFeedTask(request rssFeedRequest) ([]rssFe } func findThumbnailInItemExtensions(item *gofeed.Item) string { - media, ok := item.Extensions["media"] + media, ok := item.Extensions["media"] + if !ok { + enclosures := item.Enclosures + if len(enclosures) == 0 { + return "" + } + return recursiveFindThumbnailInEnclosures(enclosures) + } - if !ok { - return "" + return recursiveFindThumbnailInExtensions(media) +} + +func recursiveFindThumbnailInEnclosures(enclosures []*gofeed.Enclosure) string { + url := "" + + for _, enclosure := range enclosures { + switch enclosure.Type { + case "image/generic": + url = enclosure.URL + case "image/jpeg", "image/png", "image/gif": + return enclosure.URL + } } - return recursiveFindThumbnailInExtensions(media) + return url } func recursiveFindThumbnailInExtensions(extensions map[string][]gofeedext.Extension) string { @@ -361,6 +379,13 @@ func recursiveFindThumbnailInExtensions(extensions map[string][]gofeedext.Extens return url } } + if ext.Name == "link" { + if ext.Attrs["type"] == "image/jpeg" || ext.Attrs["type"] == "image/png" || ext.Attrs["type"] == "image/gif" { + if url, ok := ext.Attrs["href"]; ok { + return url + } + } + } if ext.Children != nil { if url := recursiveFindThumbnailInExtensions(ext.Children); url != "" {