diff --git a/efa_client.go b/efa_client.go index f25db0f..27573fe 100644 --- a/efa_client.go +++ b/efa_client.go @@ -1,19 +1,233 @@ package main +import "cmp" +import "encoding/json" +import "errors" +import "fmt" import "net/http" import "net/url" +import "slices" +import "strconv" import "strings" +import "time" type EFAClient interface { GetName() string BuildRequest(string) (*http.Request, error) + ParseResponse(*http.Response) (Departures, error) +} + +func FetchDepartures(c EFAClient, stopId string) (Departures, error) { + req, err := c.BuildRequest(stopId) + if err != nil { + return Departures{}, err + } + + // Send the request, wait max 10 seconds + client := http.Client{ + Timeout: 10 * time.Second, + } + res, err := client.Do(req) + if err != nil { + return Departures{}, err + } + + return c.ParseResponse(res) +} + +// JSON unmarshaling types for departure monitor API + +type DMResponse struct { + DateTime DMDateTime `json:"dateTime"` + DepartureList []DMDeparture `json:"departureList"` +} + +type DMDeparture struct { + StopName string `json:"stopName"` + Platform string `json:"platform"` + Countdown string `json:"countdown"` + DateTime DMDateTime `json:"dateTime"` + RealDateTime *DMDateTime `json:"realDateTime"` + ServingLine DMServingLine `json:"servingLine"` + OnwardStopSeq []DMStop `json:"onwardStopSeq"` +} + +type DMServingLine struct { + Symbol string `json:"symbol"` + Direction string `json:"direction"` +} + +type DMStop struct { + PlaceID string `json:"placeID"` + Place string `json:"place"` + NameWO string `json:"nameWO"` +} + +type DMDateTime struct { + Hour string `json:"hour"` + Minute string `json:"minute"` +} + +// Types for JSON marshaling + +type Departures struct { + Departures []Departure `json:"departures"` + ServerTime string `json:"serverTime"` +} + +type Departure struct { + Platform string `json:"platform"` + Symbol string `json:"symbol"` + Direction string `json:"direction"` + Leaving string `json:"leaving"` + Countdown int `json:"-"` +} + +func ParseDMResponse(res *http.Response) (Departures, error) { + defer res.Body.Close() + var dmResponse DMResponse + err := json.NewDecoder(res.Body).Decode(&dmResponse) + if err != nil { + return Departures{}, err + } + + return parseDepartures(dmResponse) +} + +func parseDepartures(response DMResponse) (Departures, error) { + var ds []Departure + + for _, d := range response.DepartureList { + direction := d.ServingLine.Direction + // If available, use name of last stop as direction + // Note that OnwardStopSeq is only populated if includeCompleteStopSeq=1 is set. + if len(d.OnwardStopSeq) != 0 { + last := d.OnwardStopSeq[len(d.OnwardStopSeq)-1] + if slices.Contains([]string{"5", "6"}, last.PlaceID) { + direction = last.NameWO + } + } + + leaving := fmt.Sprintf("%s min", d.Countdown) + countdown, err := strconv.Atoi(d.Countdown) + if err != nil { + return Departures{}, errors.New("could not parse countdown") + } + + isInThePast := countdown < 0 + if isInThePast { + continue + } + + if countdown == 0 { + leaving = "sofort" + } + if countdown > 20 { + dt := d.DateTime + if d.RealDateTime != nil { + dt = *d.RealDateTime + } + leaving = fmt.Sprintf("%02s:%02s", dt.Hour, dt.Minute) + } + + ds = append(ds, Departure{ + d.Platform, + d.ServingLine.Symbol, + direction, + leaving, + countdown, + }) + } + + slices.SortFunc(ds, func(a Departure, b Departure) int { + return cmp.Compare(a.Countdown, b.Countdown) + }) + + dt := response.DateTime + + return Departures{ + ds, + fmt.Sprintf("%02s:%02s", dt.Hour, dt.Minute), + }, nil } var allEfaClients []EFAClient = []EFAClient{ + IVBEFAClient{}, BwegtEFAClient{}, - VRNEFAClient{}, KVVEFAClient{}, VAGEFAClient{}, + VRNEFAClient{}, +} + +type IVBEFAClient struct { +} + +func (c IVBEFAClient) GetName() string { + return "IVB" +} + +func (c IVBEFAClient) BuildRequest(stopId string) (*http.Request, error) { + // curl https://smartinfo.ivb.at/api/JSON/PASSAGE'?stopID=64003&count=3' + req, err := http.NewRequest("GET", "https://smartinfo.ivb.at/api/JSON/PASSAGE", nil) + if err != nil { + return nil, err + } + + query := url.Values{} + query.Set("stopID", stopId) + req.URL.RawQuery = query.Encode() + + return req, nil +} + +func (c IVBEFAClient) ParseResponse(res *http.Response) (Departures, error) { + defer res.Body.Close() + var response []IVBResponseElem + err := json.NewDecoder(res.Body).Decode(&response) + if err != nil { + return Departures{}, err + } + + var ds []Departure + + for _, responseElem := range response { + if responseElem.Smartinfo == nil { + continue + } + si := responseElem.Smartinfo + + leaving := si.Time + if leaving == "0 min" { + leaving = "sofort" + } + + ds = append(ds, Departure{ + si.Plabel, + si.Route, + si.Direction, + leaving, + // TODO + 0, + }) + } + + return Departures{ + ds, + fmt.Sprintf("%02d:%02d", 0, 0), + }, nil +} + +// IVB smartinfo API unmarshaling types +type IVBResponseElem struct { + Smartinfo *IVBSmartInfo `json:"smartinfo"` +} + +type IVBSmartInfo struct { + Uid string `json:"uid"` + Route string `json:"route"` + Direction string `json:"direction"` + Time string `json:"time"` + Plabel string `json:"plabel"` } type VRNEFAClient struct { @@ -48,6 +262,10 @@ func (c VRNEFAClient) BuildRequest(stopId string) (*http.Request, error) { return req, nil } +func (c VRNEFAClient) ParseResponse(res *http.Response) (Departures, error) { + return ParseDMResponse(res) +} + type KVVEFAClient struct { } @@ -78,6 +296,10 @@ func (c KVVEFAClient) BuildRequest(stopId string) (*http.Request, error) { return req, nil } +func (c KVVEFAClient) ParseResponse(res *http.Response) (Departures, error) { + return ParseDMResponse(res) +} + type BwegtEFAClient struct { } @@ -110,6 +332,10 @@ func (c BwegtEFAClient) BuildRequest(stopId string) (*http.Request, error) { return req, nil } +func (c BwegtEFAClient) ParseResponse(res *http.Response) (Departures, error) { + return ParseDMResponse(res) +} + type VAGEFAClient struct { } @@ -141,3 +367,7 @@ func (c VAGEFAClient) BuildRequest(stopId string) (*http.Request, error) { return req, nil } + +func (c VAGEFAClient) ParseResponse(res *http.Response) (Departures, error) { + return ParseDMResponse(res) +} diff --git a/main.go b/main.go index 3332b42..8498fbb 100644 --- a/main.go +++ b/main.go @@ -1,148 +1,13 @@ package main -import "cmp" import "encoding/json" -import "errors" import "fmt" import "log" import "net/http" import "os" import "slices" import "strings" -import "strconv" import "sync/atomic" -import "time" - -// JSON unmarshaling types for departure monitor API - -type DMResponse struct { - DateTime DMDateTime `json:"dateTime"` - DepartureList []DMDeparture `json:"departureList"` -} - -type DMDeparture struct { - StopName string `json:"stopName"` - Platform string `json:"platform"` - Countdown string `json:"countdown"` - DateTime DMDateTime `json:"dateTime"` - RealDateTime *DMDateTime `json:"realDateTime"` - ServingLine DMServingLine `json:"servingLine"` - OnwardStopSeq []DMStop `json:"onwardStopSeq"` -} - -type DMServingLine struct { - Symbol string `json:"symbol"` - Direction string `json:"direction"` -} - -type DMStop struct { - PlaceID string `json:"placeID"` - Place string `json:"place"` - NameWO string `json:"nameWO"` -} - -type DMDateTime struct { - Hour string `json:"hour"` - Minute string `json:"minute"` -} - -func FetchDepartures(c EFAClient, stopId string) (DMResponse, error) { - req, err := c.BuildRequest(stopId) - if err != nil { - return DMResponse{}, err - } - - // Send the request, wait max 10 seconds - client := http.Client{ - Timeout: 10 * time.Second, - } - res, err := client.Do(req) - if err != nil { - return DMResponse{}, err - } - - defer res.Body.Close() - var dmResponse DMResponse - err = json.NewDecoder(res.Body).Decode(&dmResponse) - if err != nil { - return DMResponse{}, err - } - - return dmResponse, nil -} - -// Types for JSON marshaling - -type Departures struct { - Departures []Departure `json:"departures"` - ServerTime string `json:"serverTime"` -} - -type Departure struct { - Platform string `json:"platform"` - Symbol string `json:"symbol"` - Direction string `json:"direction"` - Leaving string `json:"leaving"` - Countdown int `json:"-"` -} - -func ParseDepartures(response DMResponse, allowedPlatform *string) (Departures, error) { - var ds []Departure - - for _, d := range response.DepartureList { - direction := d.ServingLine.Direction - // If available, use name of last stop as direction - // Note that OnwardStopSeq is only populated if includeCompleteStopSeq=1 is set. - if len(d.OnwardStopSeq) != 0 { - last := d.OnwardStopSeq[len(d.OnwardStopSeq)-1] - if slices.Contains([]string{"5", "6"}, last.PlaceID) { - direction = last.NameWO - } - } - - leaving := fmt.Sprintf("%s min", d.Countdown) - countdown, err := strconv.Atoi(d.Countdown) - if err != nil { - return Departures{}, errors.New("could not parse countdown") - } - - matchesPlatform := allowedPlatform == nil || d.Platform == *allowedPlatform - isInThePast := countdown < 0 - if !matchesPlatform || isInThePast { - continue - } - - if countdown == 0 { - leaving = "sofort" - } - if countdown > 20 { - dt := d.DateTime - if d.RealDateTime != nil { - dt = *d.RealDateTime - } - leaving = fmt.Sprintf("%02s:%02s", dt.Hour, dt.Minute) - } - - ds = append(ds, Departure{ - d.Platform, - d.ServingLine.Symbol, - direction, - leaving, - countdown, - }) - } - - slices.SortFunc(ds, func(a Departure, b Departure) int { - return cmp.Compare(a.Countdown, b.Countdown) - }) - - dt := response.DateTime - - return Departures{ - ds, - fmt.Sprintf("%02s:%02s", dt.Hour, dt.Minute), - }, nil -} func main() { password := os.Getenv("VRNP_PASSWORD") @@ -177,23 +42,20 @@ func main() { } stopId := query["stop_id"][0] - var platform *string = nil - if query["platform"] != nil && len(query["platform"]) != 0 { - platform = &query["platform"][0] - } + /* + var platform *string = nil + if query["platform"] != nil && len(query["platform"]) != 0 { + platform = &query["platform"][0] + } + */ - c := efaClients[(efaClient.Add(1) - 1) % uint64(len(efaClients))] - ds, err := FetchDepartures(c, stopId) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - departures, err := ParseDepartures(ds, platform) + c := efaClients[(efaClient.Add(1)-1)%uint64(len(efaClients))] + departures, err := FetchDepartures(c, stopId) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } + // TODO: Filter for platform here // Does not handle multiple media types if r.Header.Get("Accept") == "application/json" { @@ -203,7 +65,7 @@ func main() { // plain text for _, d := range departures.Departures { fmt.Fprintf(w, "%2s %-11s %6s\n", - d.Symbol, + d.Symbol[:min(2, len(d.Symbol))], d.Direction[:min(11, len(d.Direction))], d.Leaving, )