github.com/wtfutil/wtf@v0.43.0/modules/nextbus/widget.go (about) 1 package nextbus 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "net/http" 8 "strconv" 9 10 "github.com/rivo/tview" 11 "github.com/wtfutil/wtf/logger" 12 "github.com/wtfutil/wtf/view" 13 ) 14 15 // Widget is the container for your module's data 16 type Widget struct { 17 view.TextWidget 18 19 settings *Settings 20 } 21 22 // NewWidget creates and returns an instance of Widget 23 func NewWidget(tviewApp *tview.Application, redrawChan chan bool, pages *tview.Pages, settings *Settings) *Widget { 24 widget := Widget{ 25 TextWidget: view.NewTextWidget(tviewApp, redrawChan, pages, settings.common), 26 27 settings: settings, 28 } 29 return &widget 30 } 31 32 /* -------------------- Exported Functions -------------------- */ 33 34 // Refresh updates the onscreen contents of the widget 35 func (widget *Widget) Refresh() { 36 // The last call should always be to the display function 37 widget.display() 38 } 39 40 /* -------------------- Unexported Functions -------------------- */ 41 42 func (widget *Widget) content() string { 43 return getNextBus(widget.settings.agency, widget.settings.route, widget.settings.stopID) 44 } 45 46 type AutoGenerated struct { 47 Copyright string `json:"copyright"` 48 Predictions Predictions `json:"predictions"` 49 } 50 51 type Prediction struct { 52 AffectedByLayover string `json:"affectedByLayover"` 53 Seconds string `json:"seconds"` 54 TripTag string `json:"tripTag"` 55 Minutes string `json:"minutes"` 56 IsDeparture string `json:"isDeparture"` 57 Block string `json:"block"` 58 DirTag string `json:"dirTag"` 59 Branch string `json:"branch"` 60 EpochTime string `json:"epochTime"` 61 Vehicle string `json:"vehicle"` 62 } 63 64 type Direction struct { 65 PredictionRaw json.RawMessage `json:"prediction"` 66 Title string `json:"title"` 67 } 68 69 type Predictions struct { 70 RouteTag string `json:"routeTag"` 71 StopTag string `json:"stopTag"` 72 RouteTitle string `json:"routeTitle"` 73 AgencyTitle string `json:"agencyTitle"` 74 StopTitle string `json:"stopTitle"` 75 Direction Direction `json:"direction"` 76 } 77 78 func getNextBus(agency string, route string, stopID string) string { 79 url := fmt.Sprintf("https://webservices.umoiq.com/service/publicJSONFeed?command=predictions&a=%s&r=%s&stopId=%s", agency, route, stopID) 80 resp, err := http.Get(url) 81 if err != nil { 82 logger.Log(fmt.Sprintf("[nextbus] Error: Failed to make requests to umoiq for next bus predictions. Reason: %s", err)) 83 return "[nextbus] error calling umoiq" 84 } 85 body, readErr := io.ReadAll(resp.Body) 86 87 if (readErr) != nil { 88 logger.Log(fmt.Sprintf("[nextbus] Error: Failed to parse response body from umoiq. Reason: %s", err)) 89 return "[nextbus] error parsing response body" 90 } 91 92 resp.Body.Close() 93 94 var parsedResponse AutoGenerated 95 96 // partial unmarshal, we don't have r.Predictions.Direction.PredictionRaw <- YET 97 unmarshalError := json.Unmarshal(body, &parsedResponse) 98 if unmarshalError != nil { 99 logger.Log(fmt.Sprintf("[nextbus] Error: Failed to unmarshal body from umoiq. Reason: %s", err)) 100 return "[nextbus] error unmarshalling response body" 101 } 102 103 parseType := "" 104 // hacky, try object parse first 105 nextBusObject := Prediction{} 106 if err := json.Unmarshal(parsedResponse.Predictions.Direction.PredictionRaw, &nextBusObject); err == nil { 107 parseType = "object" 108 } 109 110 // if object parse failed, it probably means we have an array 111 nextBuses := []Prediction{} 112 if err := json.Unmarshal(parsedResponse.Predictions.Direction.PredictionRaw, &nextBuses); err == nil { 113 parseType = "array" 114 } 115 116 // build the final string 117 finalStr := "" 118 if parseType == "array" { 119 for _, nextBus := range nextBuses { 120 finalStr += fmt.Sprintf("%s | ETA [%s]\n", parsedResponse.Predictions.RouteTitle, strTimeToInt(nextBus.Minutes, nextBus.Seconds)) 121 } 122 } else { 123 finalStr += fmt.Sprintf("%s | ETA [%s]\n", parsedResponse.Predictions.RouteTitle, strTimeToInt(nextBusObject.Minutes, nextBusObject.Seconds)) 124 } 125 126 return finalStr 127 } 128 129 // takes minutes and seconds from the API, does math to find the remainder seconds 130 // since the API only gives whole minutes 131 func strTimeToInt(sourceMinutes string, sourceSeconds string) string { 132 min, _ := strconv.Atoi(sourceMinutes) 133 sec, _ := strconv.Atoi(sourceSeconds) 134 sec = sec % 60 135 return fmt.Sprintf("%02d:%02d", min, sec) 136 } 137 138 func (widget *Widget) display() { 139 widget.Redraw(func() (string, string, bool) { 140 return widget.CommonSettings().Title, widget.content(), false 141 }) 142 }