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 != "" {