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
26 changes: 0 additions & 26 deletions .env

This file was deleted.

2 changes: 1 addition & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func setupServices(db *database.Database, cfg *config.Config) *Services {
var exerciseService services.ExerciseService
if cfg.Wger.Enabled {
wgerClient := clients.NewWgerClient(&cfg.Wger, logger.New(cfg.Log.Level, cfg.Log.Format))
cacheService := services.NewExerciseCacheService(exerciseRepo, wgerClient, logger.New(cfg.Log.Level, cfg.Log.Format), 24*time.Hour)
cacheService := services.NewExerciseCacheService(exerciseRepo, wgerClient, logger.New(cfg.Log.Level, cfg.Log.Format), 1*time.Minute)
exerciseService = services.NewExerciseService(exerciseRepo, cacheService, logger.New(cfg.Log.Level, cfg.Log.Format))
} else {
exerciseService = nil
Expand Down
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ services:
- DB_PASSWORD=password
- DB_NAME=strive
- DB_SSL_MODE=disable
- JWT_SECRET=your-very-secure-jwt-secret-32-chars-minimum-for-local-dev
- JWT_ISSUER=strive-api
- JWT_AUDIENCE=strive-app
- JWT_CLOCK_SKEW=2m
Expand All @@ -34,7 +35,7 @@ services:
- WGER_API_ENABLED=true
- WGER_API_BASE_URL=https://wger.de/api/v2
- WGER_API_KEY=
- WGER_API_TIMEOUT=30s
- WGER_API_TIMEOUT=120s
- WGER_API_RETRY_COUNT=3
- SECURITY_HSTS_MAX_AGE=31536000
- SECURITY_HSTS_INCLUDE_SUBDOMAINS=true
Expand Down
50 changes: 44 additions & 6 deletions internal/clients/wger_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,27 +35,48 @@ func NewWgerClient(cfg *config.WgerConfig, logger *logger.Logger) *WgerClient {

func (c *WgerClient) GetAllExercises(ctx context.Context) ([]models.WgerExercise, error) {
var allExercises []models.WgerExercise
url := fmt.Sprintf("%s/exerciseinfo/?limit=100", c.baseURL)
url := fmt.Sprintf("%s/exerciseinfo/?limit=20", c.baseURL)
page := 1

c.logger.Info("Starting to fetch all exercises with pagination", "base_url", url)

for url != "" {
c.logger.Info("Fetching exercises page", "page", page, "url", url)

var response models.WgerExerciseListResponse
err := c.makeRequest(ctx, url, &response)
if err != nil {
c.logger.Error("Failed to fetch exercises page", "page", page, "error", err)
return nil, err
}

allExercises = append(allExercises, response.Results...)
c.logger.Info("Successfully fetched exercises page", "page", page, "count", len(response.Results), "total_so_far", len(allExercises))

if response.Next != nil {
url = *response.Next
page++
time.Sleep(500 * time.Millisecond)
} else {
url = ""
}
}

c.logger.Info("Successfully fetched all exercises", "total_count", len(allExercises), "total_pages", page)
return allExercises, nil
}

func (c *WgerClient) GetExercisesLimited(ctx context.Context, limit int) ([]models.WgerExercise, error) {
url := fmt.Sprintf("%s/exerciseinfo/?limit=%d", c.baseURL, limit)
var response models.WgerExerciseListResponse
err := c.makeRequest(ctx, url, &response)
if err != nil {
return nil, err
}

return response.Results, nil
}

func (c *WgerClient) GetExerciseByID(ctx context.Context, id int) (*models.WgerExercise, error) {
url := fmt.Sprintf("%s/exerciseinfo/%d/", c.baseURL, id)
var result models.WgerExercise
Expand Down Expand Up @@ -210,13 +231,17 @@ func (c *WgerClient) makeRequest(ctx context.Context, url string, result interfa
for attempt := 0; attempt <= c.retryCount; attempt++ {
if attempt > 0 {
backoff := time.Duration(attempt) * time.Second
c.logger.Debug("Retrying request", "attempt", attempt, "backoff", backoff, "url", url)
c.logger.Info("Retrying request", "attempt", attempt, "backoff", backoff, "url", url)
time.Sleep(backoff)
}

startTime := time.Now()
c.logger.Info("Starting request to wger API", "url", url, "attempt", attempt+1, "timeout", c.httpClient.Timeout)

req, err := http.NewRequestWithContext(ctx, "GET", url, http.NoBody)
if err != nil {
lastErr = fmt.Errorf("failed to create request: %w", err)
c.logger.Error("Failed to create request", "error", err, "url", url)
continue
}

Expand All @@ -226,16 +251,22 @@ func (c *WgerClient) makeRequest(ctx context.Context, url string, result interfa
req.Header.Set("Authorization", fmt.Sprintf("Token %s", c.apiKey))
}

c.logger.Debug("Making request to wger API", "url", url, "attempt", attempt+1)
c.logger.Info("Making HTTP request", "url", url, "attempt", attempt+1, "headers", req.Header)

resp, err := c.httpClient.Do(req)
requestDuration := time.Since(startTime)

if err != nil {
lastErr = fmt.Errorf("failed to make request: %w", err)
c.logger.Warn("Request failed, will retry", "error", err, "url", url, "attempt", attempt+1)
c.logger.Warn("Request failed, will retry", "error", err, "url", url, "attempt", attempt+1, "duration", requestDuration)
continue
}

c.logger.Info("Received response", "status", resp.StatusCode, "url", url, "duration", requestDuration)

if resp.StatusCode == http.StatusOK {
c.logger.Info("Reading response body", "url", url, "content_length", resp.ContentLength)

body, err := io.ReadAll(resp.Body)
_ = resp.Body.Close()

Expand All @@ -245,13 +276,20 @@ func (c *WgerClient) makeRequest(ctx context.Context, url string, result interfa
continue
}

previewSize := 200
if len(body) < previewSize {
previewSize = len(body)
}
c.logger.Info("Successfully read response body", "url", url, "body_size", len(body), "body_preview", string(body[:previewSize]))

if err := json.Unmarshal(body, &result); err != nil {
lastErr = fmt.Errorf("failed to unmarshal response: %w", err)
c.logger.Warn("Failed to unmarshal response, will retry", "error", err, "url", url, "attempt", attempt+1)
c.logger.Warn("Failed to unmarshal response, will retry",
"error", err, "url", url, "attempt", attempt+1, "body_preview", string(body[:previewSize]))
continue
}

c.logger.Debug("Successfully received response from wger API", "url", url, "attempt", attempt+1)
c.logger.Info("Successfully parsed response from wger API", "url", url, "attempt", attempt+1)
return nil
}

Expand Down
20 changes: 10 additions & 10 deletions internal/clients/wger_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,24 +61,24 @@ func TestWgerClient_GetAllExercises(t *testing.T) {
UUID: "test-uuid-1",
Name: "Bench Press",
Description: "A chest exercise",
Category: 1,
Muscles: []int{1, 2},
Equipment: []int{1},
Category: models.WgerCategory{ID: 1, Name: "Chest"},
Muscles: []models.WgerMuscle{{ID: 1, Name: "Pectorals"}, {ID: 2, Name: "Triceps"}},
Equipment: []models.WgerEquipment{{ID: 1, Name: "Barbell"}},
},
{
ID: 2,
UUID: "test-uuid-2",
Name: "Squat",
Description: "A leg exercise",
Category: 2,
Muscles: []int{3, 4},
Equipment: []int{2},
Category: models.WgerCategory{ID: 2, Name: "Legs"},
Muscles: []models.WgerMuscle{{ID: 3, Name: "Quadriceps"}, {ID: 4, Name: "Glutes"}},
Equipment: []models.WgerEquipment{{ID: 2, Name: "Barbell"}},
},
}

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/exerciseinfo/", r.URL.Path)
assert.Contains(t, r.URL.RawQuery, "limit=100")
assert.Contains(t, r.URL.RawQuery, "limit=20")

response := models.WgerExerciseListResponse{
Count: 2,
Expand Down Expand Up @@ -109,9 +109,9 @@ func TestWgerClient_GetExerciseByID(t *testing.T) {
UUID: "test-uuid",
Name: "Bench Press",
Description: "A chest exercise",
Category: 1,
Muscles: []int{1, 2},
Equipment: []int{1},
Category: models.WgerCategory{ID: 1, Name: "Chest"},
Muscles: []models.WgerMuscle{{ID: 1, Name: "Pectorals"}, {ID: 2, Name: "Triceps"}},
Equipment: []models.WgerEquipment{{ID: 1, Name: "Barbell"}},
}

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
65 changes: 47 additions & 18 deletions internal/models/wger_models.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
package models

import (
"encoding/json"
)

type WgerExercise struct {
ID int `json:"id"`
UUID string `json:"uuid"`
Name string `json:"name"`
Description string `json:"description"`
Category int `json:"category"`
Muscles []int `json:"muscles"`
MusclesSecondary []int `json:"muscles_secondary"`
Equipment []int `json:"equipment"`
Language int `json:"language"`
License int `json:"license"`
LicenseAuthor string `json:"license_author"`
Variations []int `json:"variations"`
CreationDate string `json:"creation_date"`
ID int `json:"id"`
UUID string `json:"uuid"`
Name string `json:"name"`
Description string `json:"description"`
Category WgerCategory `json:"category"`
Muscles []WgerMuscle `json:"muscles"`
MusclesSecondary []WgerMuscle `json:"muscles_secondary"`
Equipment []WgerEquipment `json:"equipment"`
Language int `json:"language"`
License WgerLicense `json:"license"`
LicenseAuthor string `json:"license_author"`
Variations json.RawMessage `json:"variations"`
CreationDate string `json:"creation_date"`
}

type WgerCategory struct {
ID int `json:"id"`
Name string `json:"name"`
}

type WgerLicense struct {
ID int `json:"id"`
FullName string `json:"full_name"`
ShortName string `json:"short_name"`
URL string `json:"url"`
}

type WgerExerciseListResponse struct {
Expand Down Expand Up @@ -49,14 +65,27 @@ type WgerEquipmentListResponse struct {
Results []WgerEquipment `json:"results"`
}

type WgerCategory struct {
ID int `json:"id"`
Name string `json:"name"`
}

type WgerCategoryListResponse struct {
Count int `json:"count"`
Next *string `json:"next"`
Previous *string `json:"previous"`
Results []WgerCategory `json:"results"`
}

func (w *WgerExercise) GetVariations() []int {
if w.Variations == nil {
return nil
}

var variations []int
if err := json.Unmarshal(w.Variations, &variations); err == nil {
return variations
}

var singleVariation int
if err := json.Unmarshal(w.Variations, &singleVariation); err == nil {
return []int{singleVariation}
}

return nil
}
Loading