prime8dev

prime8dev
git clone https://git.prime8.dev/prime8dev.git
Log | Files | Refs | README | LICENSE

wakatime.go (5723B)


      1 package external
      2 
      3 import (
      4 	"encoding/base64"
      5 	"encoding/json"
      6 	"fmt"
      7 	"io"
      8 	"net/http"
      9 	"os"
     10 	"strings"
     11 	"time"
     12 
     13 	"github.com/patrickmn/go-cache"
     14 )
     15 
     16 type WakaTime struct {
     17 	httpClient *http.Client
     18 	baseURL    string
     19 	apiKey     string
     20 	cache      *cache.Cache
     21 }
     22 
     23 func NewWakaTime(c *cache.Cache) *WakaTime {
     24 	return &WakaTime{
     25 		httpClient: &http.Client{
     26 			Timeout: 10 * time.Second,
     27 		},
     28 		baseURL: "https://wakatime.com/api/v1",
     29 		apiKey:  strings.TrimSpace(os.Getenv("WAKATIME_API_KEY")),
     30 		cache:   c,
     31 	}
     32 }
     33 
     34 type WakaTimeStats struct {
     35 	AllTime AllTimeStats
     36 	Weekly  WeeklyStats
     37 }
     38 
     39 func (s *WakaTime) FetchStats() (*WakaTimeStats, error) {
     40 	if s.apiKey == "" {
     41 		return nil, nil
     42 	}
     43 
     44 	cacheKey := "wakatime:stats"
     45 
     46 	if val, found := s.cache.Get(cacheKey); found {
     47 		return val.(*WakaTimeStats), nil
     48 	}
     49 
     50 	allTime, err := s.fetchAllTimeStats()
     51 	if err != nil {
     52 		return nil, err
     53 	}
     54 
     55 	weekly, err := s.fetchWeeklyStats()
     56 	if err != nil {
     57 		return nil, err
     58 	}
     59 
     60 	stats := &WakaTimeStats{
     61 		AllTime: *allTime,
     62 		Weekly:  *weekly,
     63 	}
     64 
     65 	s.cache.Set(cacheKey, stats, 5*time.Minute)
     66 	return stats, nil
     67 }
     68 
     69 type allTimeResponse struct {
     70 	Data struct {
     71 		TotalSeconds float64 `json:"total_seconds"`
     72 	} `json:"data"`
     73 }
     74 
     75 type languagesResponse struct {
     76 	Data struct {
     77 		DailyAverageIncludingOtherLanguage float64 `json:"daily_average_including_other_language"`
     78 	} `json:"data"`
     79 }
     80 
     81 type AllTimeStats struct {
     82 	TotalText        string
     83 	DailyAverageText string
     84 }
     85 
     86 func (s *WakaTime) fetchAllTimeStats() (*AllTimeStats, error) {
     87 	cacheKey := "wakatime:alltime"
     88 
     89 	if val, found := s.cache.Get(cacheKey); found {
     90 		return val.(*AllTimeStats), nil
     91 	}
     92 
     93 	statsCh := make(chan *allTimeResponse, 1)
     94 	langCh := make(chan *languagesResponse, 1)
     95 	errCh := make(chan error, 2)
     96 
     97 	go func() {
     98 		resp, err := s.makeRequest("/users/current/all_time_since_today")
     99 		if err != nil {
    100 			errCh <- err
    101 			return
    102 		}
    103 		var data allTimeResponse
    104 		if err := json.Unmarshal(resp, &data); err != nil {
    105 			errCh <- err
    106 			return
    107 		}
    108 		statsCh <- &data
    109 	}()
    110 
    111 	go func() {
    112 		resp, err := s.makeRequest("/users/current/stats/last_year")
    113 		if err != nil {
    114 			errCh <- err
    115 			return
    116 		}
    117 		var data languagesResponse
    118 		if err := json.Unmarshal(resp, &data); err != nil {
    119 			errCh <- err
    120 			return
    121 		}
    122 		langCh <- &data
    123 	}()
    124 
    125 	var stats *allTimeResponse
    126 	var langStats *languagesResponse
    127 
    128 	for range 2 {
    129 		select {
    130 		case s := <-statsCh:
    131 			stats = s
    132 		case l := <-langCh:
    133 			langStats = l
    134 		case err := <-errCh:
    135 			return nil, err
    136 		}
    137 	}
    138 
    139 	totalSeconds := 0.0
    140 	if stats != nil {
    141 		totalSeconds = stats.Data.TotalSeconds
    142 	}
    143 	totalHours := int(totalSeconds / 3600)
    144 	totalText := fmt.Sprintf("%d+ hrs coded", totalHours)
    145 
    146 	dailyAvgText := "0 mins"
    147 	if langStats != nil && langStats.Data.DailyAverageIncludingOtherLanguage > 0 {
    148 		dailyAvgText = formatDuration(langStats.Data.DailyAverageIncludingOtherLanguage)
    149 	}
    150 
    151 	result := &AllTimeStats{
    152 		TotalText:        totalText,
    153 		DailyAverageText: dailyAvgText,
    154 	}
    155 
    156 	s.cache.Set(cacheKey, result, 5*time.Minute)
    157 	return result, nil
    158 }
    159 
    160 func formatDuration(seconds float64) string {
    161 	hours := int(seconds / 3600)
    162 	minutes := int((seconds - float64(hours)*3600) / 60)
    163 
    164 	if hours > 0 {
    165 		return fmt.Sprintf("%d hrs %d mins", hours, minutes)
    166 	}
    167 	return fmt.Sprintf("%d mins", minutes)
    168 }
    169 
    170 type summariesResponse struct {
    171 	Data []struct {
    172 		Range struct {
    173 			Date string `json:"date"`
    174 		} `json:"range"`
    175 		GrandTotal struct {
    176 			TotalSeconds float64 `json:"total_seconds"`
    177 		} `json:"grand_total"`
    178 	} `json:"data"`
    179 }
    180 
    181 type WeeklyStats struct {
    182 	TotalText string
    183 	Days      []DailyEntry
    184 }
    185 
    186 type DailyEntry struct {
    187 	DayText         string
    188 	DateText        string
    189 	TimeText        string
    190 	BarWidthPercent string
    191 }
    192 
    193 func (s *WakaTime) fetchWeeklyStats() (*WeeklyStats, error) {
    194 	cacheKey := "wakatime:weekly"
    195 
    196 	if val, found := s.cache.Get(cacheKey); found {
    197 		return val.(*WeeklyStats), nil
    198 	}
    199 
    200 	end := time.Now()
    201 	start := end.AddDate(0, 0, -6)
    202 
    203 	url := fmt.Sprintf("/users/current/summaries?start=%s&end=%s",
    204 		start.Format("2006-01-02"),
    205 		end.Format("2006-01-02"))
    206 
    207 	resp, err := s.makeRequest(url)
    208 	if err != nil {
    209 		return nil, err
    210 	}
    211 
    212 	var data summariesResponse
    213 	if err := json.Unmarshal(resp, &data); err != nil {
    214 		return nil, err
    215 	}
    216 
    217 	days := make([]DailyEntry, 0, len(data.Data))
    218 	var totalSeconds float64
    219 	var maxMinutes = 1
    220 	minutesPerDay := make([]int, len(data.Data))
    221 
    222 	for i, day := range data.Data {
    223 		date, _ := time.Parse("2006-01-02", day.Range.Date)
    224 
    225 		dayText := date.Weekday().String()[:3]
    226 		dateText := fmt.Sprintf("%s %d", date.Month().String()[:3], date.Day())
    227 
    228 		seconds := day.GrandTotal.TotalSeconds
    229 		totalSeconds += seconds
    230 
    231 		minutes := int(seconds / 60)
    232 		minutesPerDay[i] = minutes
    233 		if minutes > maxMinutes {
    234 			maxMinutes = minutes
    235 		}
    236 
    237 		timeText := formatDuration(seconds)
    238 
    239 		days = append(days, DailyEntry{
    240 			DayText:  dayText,
    241 			DateText: dateText,
    242 			TimeText: timeText,
    243 		})
    244 	}
    245 
    246 	for i := range days {
    247 		days[i].BarWidthPercent = fmt.Sprintf("%d%%", (minutesPerDay[i]*100)/maxMinutes)
    248 	}
    249 
    250 	result := &WeeklyStats{
    251 		TotalText: formatDuration(totalSeconds),
    252 		Days:      days,
    253 	}
    254 
    255 	s.cache.Set(cacheKey, result, 5*time.Minute)
    256 	return result, nil
    257 }
    258 
    259 func (s *WakaTime) makeRequest(path string) ([]byte, error) {
    260 	url := s.baseURL + path
    261 
    262 	req, err := http.NewRequest("GET", url, nil)
    263 	if err != nil {
    264 		return nil, err
    265 	}
    266 
    267 	auth := base64.StdEncoding.EncodeToString([]byte(s.apiKey))
    268 	req.Header.Set("Authorization", fmt.Sprintf("Basic %s", auth))
    269 
    270 	resp, err := s.httpClient.Do(req)
    271 	if err != nil {
    272 		return nil, err
    273 	}
    274 	defer resp.Body.Close()
    275 
    276 	if resp.StatusCode != http.StatusOK {
    277 		return nil, fmt.Errorf("wakatime API returned %d", resp.StatusCode)
    278 	}
    279 
    280 	return io.ReadAll(resp.Body)
    281 }