Bläddra i källkod

Update releases widget

Svilen Markov 6 månader sedan
förälder
incheckning
0ce706e02e
4 ändrade filer med 116 tillägg och 246 borttagningar
  1. 13 0
      docs/configuration.md
  2. 0 73
      internal/feed/releases.go
  3. 103 68
      internal/glance/widget-releases.go
  4. 0 105
      internal/widget/releases.go

+ 13 - 0
docs/configuration.md

@@ -1332,6 +1332,19 @@ repositories:
   - dockerhub:nginx:stable-alpine
 ```
 
+To include prereleases you can specify the repository as an object and use the `include-prereleases` property:
+
+**Note: This feature is currently only available for GitHub repositories.**
+
+```yaml
+repositories:
+  - gitlab:inkscape/inkscape
+  - repository: glanceapp/glance
+    include-prereleases: true
+  - codeberg:redict/redict
+```
+
+
 
 ##### `show-source-icon`
 Shows an icon of the source (GitHub/GitLab/Codeberg/Docker Hub) next to the repository name when set to `true`.

+ 0 - 73
internal/feed/releases.go

@@ -1,73 +0,0 @@
-package feed
-
-import (
-	"errors"
-	"fmt"
-	"log/slog"
-)
-
-type ReleaseSource string
-
-const (
-	ReleaseSourceCodeberg  ReleaseSource = "codeberg"
-	ReleaseSourceGithub    ReleaseSource = "github"
-	ReleaseSourceGitlab    ReleaseSource = "gitlab"
-	ReleaseSourceDockerHub ReleaseSource = "dockerhub"
-)
-
-type ReleaseRequest struct {
-	Source                   ReleaseSource
-	Repository               string
-	Token                    *string
-	IncludeGithubPreReleases bool
-}
-
-func FetchLatestReleases(requests []*ReleaseRequest) (AppReleases, error) {
-	job := newJob(fetchLatestReleaseTask, requests).withWorkers(20)
-	results, errs, err := workerPoolDo(job)
-
-	if err != nil {
-		return nil, err
-	}
-
-	var failed int
-
-	releases := make(AppReleases, 0, len(requests))
-
-	for i := range results {
-		if errs[i] != nil {
-			failed++
-			slog.Error("Failed to fetch release", "source", requests[i].Source, "repository", requests[i].Repository, "error", errs[i])
-			continue
-		}
-
-		releases = append(releases, *results[i])
-	}
-
-	if failed == len(requests) {
-		return nil, ErrNoContent
-	}
-
-	releases.SortByNewest()
-
-	if failed > 0 {
-		return releases, fmt.Errorf("%w: could not get %d releases", ErrPartialContent, failed)
-	}
-
-	return releases, nil
-}
-
-func fetchLatestReleaseTask(request *ReleaseRequest) (*AppRelease, error) {
-	switch request.Source {
-	case ReleaseSourceCodeberg:
-		return fetchLatestCodebergRelease(request)
-	case ReleaseSourceGithub:
-		return fetchLatestGithubRelease(request)
-	case ReleaseSourceGitlab:
-		return fetchLatestGitLabRelease(request)
-	case ReleaseSourceDockerHub:
-		return fetchLatestDockerHubRelease(request)
-	}
-
-	return nil, errors.New("unsupported source")
-}

+ 103 - 68
internal/glance/widget-releases.go

@@ -11,20 +11,21 @@ import (
 	"sort"
 	"strings"
 	"time"
+
+	"gopkg.in/yaml.v3"
 )
 
 var releasesWidgetTemplate = mustParseTemplate("releases.html", "widget-base.html")
 
 type releasesWidget struct {
-	widgetBase      `yaml:",inline"`
-	Releases        appReleaseList    `yaml:"-"`
-	releaseRequests []*releaseRequest `yaml:"-"`
-	Repositories    []string          `yaml:"repositories"`
-	Token           string            `yaml:"token"`
-	GitLabToken     string            `yaml:"gitlab-token"`
-	Limit           int               `yaml:"limit"`
-	CollapseAfter   int               `yaml:"collapse-after"`
-	ShowSourceIcon  bool              `yaml:"show-source-icon"`
+	widgetBase     `yaml:",inline"`
+	Releases       appReleaseList    `yaml:"-"`
+	Repositories   []*releaseRequest `yaml:"repositories"`
+	Token          string            `yaml:"token"`
+	GitLabToken    string            `yaml:"gitlab-token"`
+	Limit          int               `yaml:"limit"`
+	CollapseAfter  int               `yaml:"collapse-after"`
+	ShowSourceIcon bool              `yaml:"show-source-icon"`
 }
 
 func (widget *releasesWidget) initialize() error {
@@ -38,51 +39,21 @@ func (widget *releasesWidget) initialize() error {
 		widget.CollapseAfter = 5
 	}
 
-	for _, repository := range widget.Repositories {
-		parts := strings.SplitN(repository, ":", 2)
-		var request *releaseRequest
-		if len(parts) == 1 {
-			request = &releaseRequest{
-				source:     releaseSourceGithub,
-				repository: repository,
-			}
-
-			if widget.Token != "" {
-				request.token = &widget.Token
-			}
-		} else if len(parts) == 2 {
-			if parts[0] == string(releaseSourceGitlab) {
-				request = &releaseRequest{
-					source:     releaseSourceGitlab,
-					repository: parts[1],
-				}
-
-				if widget.GitLabToken != "" {
-					request.token = &widget.GitLabToken
-				}
-			} else if parts[0] == string(releaseSourceDockerHub) {
-				request = &releaseRequest{
-					source:     releaseSourceDockerHub,
-					repository: parts[1],
-				}
-			} else if parts[0] == string(releaseSourceCodeberg) {
-				request = &releaseRequest{
-					source:     releaseSourceCodeberg,
-					repository: parts[1],
-				}
-			} else {
-				return errors.New("invalid repository source " + parts[0])
-			}
-		}
+	for i := range widget.Repositories {
+		r := widget.Repositories[i]
 
-		widget.releaseRequests = append(widget.releaseRequests, request)
+		if r.source == releaseSourceGithub && widget.Token != "" {
+			r.token = &widget.Token
+		} else if r.source == releaseSourceGitlab && widget.GitLabToken != "" {
+			r.token = &widget.GitLabToken
+		}
 	}
 
 	return nil
 }
 
 func (widget *releasesWidget) update(ctx context.Context) {
-	releases, err := fetchLatestReleases(widget.releaseRequests)
+	releases, err := fetchLatestReleases(widget.Repositories)
 
 	if !widget.canContinueUpdateAfterHandlingErr(err) {
 		return
@@ -133,9 +104,53 @@ func (r appReleaseList) sortByNewest() appReleaseList {
 }
 
 type releaseRequest struct {
-	source     releaseSource
-	repository string
-	token      *string
+	IncludePreleases bool   `yaml:"include-prereleases"`
+	Repository       string `yaml:"repository"`
+
+	source releaseSource
+	token  *string
+}
+
+func (r *releaseRequest) UnmarshalYAML(node *yaml.Node) error {
+	type releaseRequestAlias releaseRequest
+	alias := (*releaseRequestAlias)(r)
+	var repository string
+
+	if err := node.Decode(&repository); err != nil {
+		if err := node.Decode(alias); err != nil {
+			return fmt.Errorf("could not umarshal repository into string or struct: %v", err)
+		}
+	}
+
+	if r.Repository == "" {
+		if repository == "" {
+			return errors.New("repository is required")
+		} else {
+			r.Repository = repository
+		}
+	}
+
+	parts := strings.SplitN(repository, ":", 2)
+	if len(parts) == 1 {
+		r.source = releaseSourceGithub
+	} else if len(parts) == 2 {
+		r.Repository = parts[1]
+
+		switch parts[0] {
+		case string(releaseSourceGithub):
+			r.source = releaseSourceGithub
+		case string(releaseSourceGitlab):
+			r.source = releaseSourceGitlab
+		case string(releaseSourceDockerHub):
+			r.source = releaseSourceDockerHub
+		case string(releaseSourceCodeberg):
+			r.source = releaseSourceCodeberg
+		default:
+			return errors.New("invalid source")
+		}
+	}
+
+	return nil
 }
 
 func fetchLatestReleases(requests []*releaseRequest) (appReleaseList, error) {
@@ -152,7 +167,7 @@ func fetchLatestReleases(requests []*releaseRequest) (appReleaseList, error) {
 	for i := range results {
 		if errs[i] != nil {
 			failed++
-			slog.Error("Failed to fetch release", "source", requests[i].source, "repository", requests[i].repository, "error", errs[i])
+			slog.Error("Failed to fetch release", "source", requests[i].source, "repository", requests[i].Repository, "error", errs[i])
 			continue
 		}
 
@@ -187,7 +202,7 @@ func fetchLatestReleaseTask(request *releaseRequest) (*appRelease, error) {
 	return nil, errors.New("unsupported source")
 }
 
-type githubReleaseLatestResponseJson struct {
+type githubReleaseResponseJson struct {
 	TagName     string `json:"tag_name"`
 	PublishedAt string `json:"published_at"`
 	HtmlUrl     string `json:"html_url"`
@@ -197,12 +212,17 @@ type githubReleaseLatestResponseJson struct {
 }
 
 func fetchLatestGithubRelease(request *releaseRequest) (*appRelease, error) {
-	httpRequest, err := http.NewRequest(
-		"GET",
-		fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", request.repository),
-		nil,
-	)
+	var requestURL string
+
+	if !request.IncludePreleases {
+		requestURL = fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", request.Repository)
+	} else {
+		requestURL = fmt.Sprintf("https://api.github.com/repos/%s/releases", request.Repository)
+	}
 
+	fmt.Println(requestURL)
+
+	httpRequest, err := http.NewRequest("GET", requestURL, nil)
 	if err != nil {
 		return nil, err
 	}
@@ -211,14 +231,29 @@ func fetchLatestGithubRelease(request *releaseRequest) (*appRelease, error) {
 		httpRequest.Header.Add("Authorization", "Bearer "+(*request.token))
 	}
 
-	response, err := decodeJsonFromRequest[githubReleaseLatestResponseJson](defaultHTTPClient, httpRequest)
-	if err != nil {
-		return nil, err
+	var response githubReleaseResponseJson
+
+	if !request.IncludePreleases {
+		response, err = decodeJsonFromRequest[githubReleaseResponseJson](defaultHTTPClient, httpRequest)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		responses, err := decodeJsonFromRequest[[]githubReleaseResponseJson](defaultHTTPClient, httpRequest)
+		if err != nil {
+			return nil, err
+		}
+
+		if len(responses) == 0 {
+			return nil, fmt.Errorf("no releases found for repository %s", request.Repository)
+		}
+
+		response = responses[0]
 	}
 
 	return &appRelease{
 		Source:       releaseSourceGithub,
-		Name:         request.repository,
+		Name:         request.Repository,
 		Version:      normalizeVersionFormat(response.TagName),
 		NotesUrl:     response.HtmlUrl,
 		TimeReleased: parseRFC3339Time(response.PublishedAt),
@@ -242,10 +277,10 @@ const dockerHubSpecificTagURLFormat = "https://hub.docker.com/v2/namespaces/%s/r
 
 func fetchLatestDockerHubRelease(request *releaseRequest) (*appRelease, error) {
 
-	nameParts := strings.Split(request.repository, "/")
+	nameParts := strings.Split(request.Repository, "/")
 
 	if len(nameParts) > 2 {
-		return nil, fmt.Errorf("invalid repository name: %s", request.repository)
+		return nil, fmt.Errorf("invalid repository name: %s", request.Repository)
 	} else if len(nameParts) == 1 {
 		nameParts = []string{"library", nameParts[0]}
 	}
@@ -278,7 +313,7 @@ func fetchLatestDockerHubRelease(request *releaseRequest) (*appRelease, error) {
 		}
 
 		if len(response.Results) == 0 {
-			return nil, fmt.Errorf("no tags found for repository: %s", request.repository)
+			return nil, fmt.Errorf("no tags found for repository: %s", request.Repository)
 		}
 
 		tag = &response.Results[0]
@@ -331,7 +366,7 @@ func fetchLatestGitLabRelease(request *releaseRequest) (*appRelease, error) {
 		"GET",
 		fmt.Sprintf(
 			"https://gitlab.com/api/v4/projects/%s/releases/permalink/latest",
-			url.QueryEscape(request.repository),
+			url.QueryEscape(request.Repository),
 		),
 		nil,
 	)
@@ -350,7 +385,7 @@ func fetchLatestGitLabRelease(request *releaseRequest) (*appRelease, error) {
 
 	return &appRelease{
 		Source:       releaseSourceGitlab,
-		Name:         request.repository,
+		Name:         request.Repository,
 		Version:      normalizeVersionFormat(response.TagName),
 		NotesUrl:     response.Links.Self,
 		TimeReleased: parseRFC3339Time(response.ReleasedAt),
@@ -368,7 +403,7 @@ func fetchLatestCodebergRelease(request *releaseRequest) (*appRelease, error) {
 		"GET",
 		fmt.Sprintf(
 			"https://codeberg.org/api/v1/repos/%s/releases/latest",
-			request.repository,
+			request.Repository,
 		),
 		nil,
 	)
@@ -383,7 +418,7 @@ func fetchLatestCodebergRelease(request *releaseRequest) (*appRelease, error) {
 
 	return &appRelease{
 		Source:       releaseSourceCodeberg,
-		Name:         request.repository,
+		Name:         request.Repository,
 		Version:      normalizeVersionFormat(response.TagName),
 		NotesUrl:     response.HtmlUrl,
 		TimeReleased: parseRFC3339Time(response.PublishedAt),

+ 0 - 105
internal/widget/releases.go

@@ -1,105 +0,0 @@
-package widget
-
-import (
-	"context"
-	"errors"
-	"html/template"
-	"strings"
-	"time"
-
-	"github.com/glanceapp/glance/internal/assets"
-	"github.com/glanceapp/glance/internal/feed"
-)
-
-type Releases struct {
-	widgetBase               `yaml:",inline"`
-	Releases                 feed.AppReleases       `yaml:"-"`
-	releaseRequests          []*feed.ReleaseRequest `yaml:"-"`
-	Repositories             []string               `yaml:"repositories"`
-	Token                    OptionalEnvString      `yaml:"token"`
-	GitLabToken              OptionalEnvString      `yaml:"gitlab-token"`
-	Limit                    int                    `yaml:"limit"`
-	CollapseAfter            int                    `yaml:"collapse-after"`
-	ShowSourceIcon           bool                   `yaml:"show-source-icon"`
-	IncludeGithubPreReleases bool                   `yaml:"include-github-prereleases"`
-}
-
-func (widget *Releases) Initialize() error {
-	widget.withTitle("Releases").withCacheDuration(2 * time.Hour)
-
-	if widget.Limit <= 0 {
-		widget.Limit = 10
-	}
-
-	if widget.CollapseAfter == 0 || widget.CollapseAfter < -1 {
-		widget.CollapseAfter = 5
-	}
-
-	var tokenAsString = widget.Token.String()
-	var gitLabTokenAsString = widget.GitLabToken.String()
-
-	for _, repository := range widget.Repositories {
-		parts := strings.SplitN(repository, ":", 2)
-		var request *feed.ReleaseRequest
-		if len(parts) == 1 {
-			request = &feed.ReleaseRequest{
-				Source:                   feed.ReleaseSourceGithub,
-				Repository:               repository,
-				IncludeGithubPreReleases: widget.IncludeGithubPreReleases,
-			}
-
-			if widget.Token != "" {
-				request.Token = &tokenAsString
-			}
-		} else if len(parts) == 2 {
-			if parts[0] == string(feed.ReleaseSourceGitlab) {
-				request = &feed.ReleaseRequest{
-					Source:     feed.ReleaseSourceGitlab,
-					Repository: parts[1],
-				}
-
-				if widget.GitLabToken != "" {
-					request.Token = &gitLabTokenAsString
-				}
-			} else if parts[0] == string(feed.ReleaseSourceDockerHub) {
-				request = &feed.ReleaseRequest{
-					Source:     feed.ReleaseSourceDockerHub,
-					Repository: parts[1],
-				}
-			} else if parts[0] == string(feed.ReleaseSourceCodeberg) {
-				request = &feed.ReleaseRequest{
-					Source:     feed.ReleaseSourceCodeberg,
-					Repository: parts[1],
-				}
-			} else {
-				return errors.New("invalid repository source " + parts[0])
-			}
-		}
-
-		widget.releaseRequests = append(widget.releaseRequests, request)
-	}
-
-	return nil
-}
-
-func (widget *Releases) Update(ctx context.Context) {
-	releases, err := feed.FetchLatestReleases(widget.releaseRequests)
-
-	if !widget.canContinueUpdateAfterHandlingErr(err) {
-		return
-	}
-
-	if len(releases) > widget.Limit {
-		releases = releases[:widget.Limit]
-	}
-
-	for i := range releases {
-		releases[i].SourceIconURL = widget.Providers.AssetResolver("icons/" + string(releases[i].Source) + ".svg")
-	}
-
-	widget.Releases = releases
-}
-
-func (widget *Releases) Render() template.HTML {
-	return widget.render(widget, assets.ReleasesTemplate)
-}