Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions internal/glance/templates/releases.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
{{ if gt .Downvotes 3 }}
<li>{{ .Downvotes | formatNumber }} ⚠</li>
{{ end }}
{{ if .CurrentVersion }}
<li>({{ .CurrentVersion }})</li>
{{ end }}
</ul>
</li>
{{ end }}
Expand Down
88 changes: 75 additions & 13 deletions internal/glance/widget-releases.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import (
"errors"
"fmt"
"html/template"
"io"
"log/slog"
"net/http"
"net/url"
"sort"
"strings"
"time"

"github.com/tidwall/gjson"
"gopkg.in/yaml.v3"
)

Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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 += "... <truncated>"
}

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 {
Expand Down
35 changes: 30 additions & 5 deletions internal/glance/widget-rss.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 != "" {
Expand Down