package backend import ( "context" "encoding/json" "fmt" "net/http" "strings" "sync" "time" "music-server/internal/logging" "go.uber.org/zap" ) const releaseCacheTTL = 5 * time.Minute type giteaRelease struct { Id int `json:"id"` Name string `json:"name"` Assets []giteaAsset `json:"assets"` } type giteaAsset struct { Id int `json:"id"` Name string `json:"name"` DownloadUrl string `json:"browser_download_url"` } // ReleaseService looks up the latest client release on Gitea. type ReleaseService struct { url string client *http.Client now func() time.Time mu sync.Mutex cached giteaRelease fetchedAt time.Time } // NewReleaseService creates a ReleaseService that reads the latest release from url. func NewReleaseService(url string) *ReleaseService { return &ReleaseService{url: url, client: &http.Client{Timeout: 10 * time.Second}, now: time.Now} } func (r *ReleaseService) latest(ctx context.Context) (giteaRelease, error) { r.mu.Lock() defer r.mu.Unlock() if !r.fetchedAt.IsZero() && r.now().Sub(r.fetchedAt) < releaseCacheTTL { return r.cached, nil } release, err := r.fetch(ctx) if err != nil { logging.GetLogger().Error("Failed to fetch latest release", zap.String("url", r.url), zap.Error(err)) return giteaRelease{}, fmt.Errorf("%w: %v", ErrUpstream, err) } r.cached, r.fetchedAt = release, r.now() return release, nil } func (r *ReleaseService) fetch(ctx context.Context) (giteaRelease, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, r.url, nil) if err != nil { return giteaRelease{}, err } resp, err := r.client.Do(req) if err != nil { return giteaRelease{}, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return giteaRelease{}, fmt.Errorf("unexpected status %s", resp.Status) } var release giteaRelease if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { return giteaRelease{}, err } return release, nil } // LatestVersion returns the name of the latest release. func (r *ReleaseService) LatestVersion(ctx context.Context) (string, error) { release, err := r.latest(ctx) return release.Name, err } // AssetNames returns the names of all assets of the latest release. func (r *ReleaseService) AssetNames(ctx context.Context) ([]string, error) { release, err := r.latest(ctx) if err != nil { return nil, err } names := make([]string, len(release.Assets)) for i, asset := range release.Assets { names[i] = asset.Name } return names, nil } // AssetURL returns the download URL of the first asset of the latest release with the given suffix. func (r *ReleaseService) AssetURL(ctx context.Context, suffix string) (string, error) { release, err := r.latest(ctx) if err != nil { return "", err } for _, asset := range release.Assets { if strings.HasSuffix(asset.Name, suffix) { return asset.DownloadUrl, nil } } return "", ErrNotFound }