convert the server to golang #1
11 changed files with 250 additions and 1887 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1 +1 @@
|
||||||
node_modules
|
omnibus
|
||||||
|
|
32
config.go
Normal file
32
config.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
|
||||||
|
// network port to listen on
|
||||||
|
var serverPort = 8080
|
||||||
|
|
||||||
|
|
||||||
|
// which cardinal direction (n,e,s,w) is the display facing? (not the person looking at it)
|
||||||
|
var screenDir = "w"
|
||||||
|
|
||||||
|
|
||||||
|
// which routes do you want to show on the screen? add as many or as few as you'd like.
|
||||||
|
var routes = []Route{
|
||||||
|
Route{
|
||||||
|
Id: 15280,
|
||||||
|
Name: "46 WB",
|
||||||
|
Dir: "n",
|
||||||
|
},
|
||||||
|
Route{
|
||||||
|
Id: 15412,
|
||||||
|
Name: "46 EB",
|
||||||
|
Dir: "s",
|
||||||
|
},
|
||||||
|
Route{
|
||||||
|
Id: 51430,
|
||||||
|
Name: "Blue line",
|
||||||
|
},
|
||||||
|
Route{
|
||||||
|
Id: 51415,
|
||||||
|
Name: "Blue line",
|
||||||
|
},
|
||||||
|
}
|
33
config.js
33
config.js
|
@ -1,33 +0,0 @@
|
||||||
// ignore this line
|
|
||||||
module.exports = {}
|
|
||||||
|
|
||||||
module.exports.port = 8080
|
|
||||||
|
|
||||||
// which cardinal direction (n,e,s,w) is the display facing? (not the person looking at it)
|
|
||||||
module.exports.screenDir = 'w'
|
|
||||||
|
|
||||||
// which routes do you want to show on the screen? add as many or as few as you'd like.
|
|
||||||
module.exports.routes = [
|
|
||||||
// id stop ID shown on sign or google maps
|
|
||||||
// name friendly readable name (optional)
|
|
||||||
// dir cardinal direction the bus or train is going (optional, will use value from API)
|
|
||||||
// ~~route filter by route name (optional)~~ not yet implemented
|
|
||||||
{
|
|
||||||
id: '15280',
|
|
||||||
name: '46 WB',
|
|
||||||
dir: 'n',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '15412',
|
|
||||||
name: '46 EB',
|
|
||||||
dir: 's',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '51430',
|
|
||||||
name: 'Blue line',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '51415',
|
|
||||||
name: 'Blue line',
|
|
||||||
},
|
|
||||||
]
|
|
3
go.mod
Normal file
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module git.cyberia.club/reese/omnibus
|
||||||
|
|
||||||
|
go 1.19
|
142
main.go
Normal file
142
main.go
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
type Route struct {
|
||||||
|
Id uint // stop ID shown on sign or google maps
|
||||||
|
Name string // friendly readable name (optional)
|
||||||
|
Dir string // cardinal direction the bus or train is going (optional, will use value from API)
|
||||||
|
// Route string // filter by route name (optional) /!\ not yet implemented /!\
|
||||||
|
}
|
||||||
|
|
||||||
|
type Eta struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Dir string `json:"dir"`
|
||||||
|
Class string `json:"class"`
|
||||||
|
Delta string `json:"delta"`
|
||||||
|
}
|
||||||
|
var NilEta = Eta{}
|
||||||
|
|
||||||
|
type StrSlice []string
|
||||||
|
func (slice StrSlice) indexOf(value string) int {
|
||||||
|
for p, v := range slice {
|
||||||
|
if (v == value) {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var cardinals = StrSlice{"n","e","s","w"}
|
||||||
|
var bounds = StrSlice{"NB","EB","SB","WB"}
|
||||||
|
var arrows = StrSlice{"↑","->","↓","<-"}
|
||||||
|
func arrow(dir string) string {
|
||||||
|
var format *StrSlice
|
||||||
|
if len(dir) < 2 {
|
||||||
|
format = &cardinals
|
||||||
|
} else {
|
||||||
|
format = &bounds
|
||||||
|
}
|
||||||
|
going := (format.indexOf(dir) + cardinals.indexOf(screenDir)) % 4
|
||||||
|
return arrows[going]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (r Route) getETA() Eta {
|
||||||
|
d := fetchRouteData(r.Id)
|
||||||
|
e := Eta{}
|
||||||
|
if len(d.Departures) == 0 {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
next := d.Departures[0]
|
||||||
|
|
||||||
|
// route name
|
||||||
|
if r.Name == "" {
|
||||||
|
e.Name = next.RouteShortName
|
||||||
|
} else {
|
||||||
|
e.Name = r.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// direction arrow
|
||||||
|
if r.Dir == "" {
|
||||||
|
e.Dir = arrow(next.DirectionText)
|
||||||
|
} else {
|
||||||
|
e.Dir = arrow(r.Dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// time delta
|
||||||
|
t := time.Unix(next.DepartureTime, 0)
|
||||||
|
dur := time.Until(t)
|
||||||
|
if dur.Minutes() <= 1 {
|
||||||
|
e.Delta = "Due"
|
||||||
|
} else if dur.Hours() < 60 {
|
||||||
|
e.Delta = fmt.Sprintf("%.0f minutes", dur.Minutes())
|
||||||
|
} else {
|
||||||
|
e.Delta = fmt.Sprintf("%.0fh %.0fm", dur.Hours(), dur.Minutes() - (math.Floor(dur.Hours())*60))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// css class name
|
||||||
|
if dur.Minutes() <= 1 {
|
||||||
|
e.Class = "now"
|
||||||
|
} else if dur.Minutes() <= 3 {
|
||||||
|
e.Class = "soon"
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func main(){
|
||||||
|
http.Handle("/", http.FileServer(http.Dir("./static")))
|
||||||
|
http.HandleFunc("/etas", api)
|
||||||
|
fmt.Printf("Starting server on port %d\n", serverPort)
|
||||||
|
if err := http.ListenAndServe(fmt.Sprintf(":%d", serverPort), nil); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func api(response http.ResponseWriter, request *http.Request){
|
||||||
|
var etas []Eta
|
||||||
|
|
||||||
|
for _, route := range routes {
|
||||||
|
eta := route.getETA()
|
||||||
|
if eta != NilEta {
|
||||||
|
etas = append(etas, eta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(etas)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprint(response, "[]")
|
||||||
|
} else {
|
||||||
|
fmt.Fprint(response, string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: cache the ETA list
|
||||||
|
|
||||||
|
// let etaCache = {
|
||||||
|
// value: null,
|
||||||
|
// createdAt: 0,
|
||||||
|
// }
|
||||||
|
// let getETAsFromCache = async (maxAge) => {
|
||||||
|
// if (Date.now() / 1000 - etaCache.createdAt >= maxAge) {
|
||||||
|
// etaCache.value = []
|
||||||
|
// for (let r of conf.routes) {
|
||||||
|
// let e = await getETA(r)
|
||||||
|
// if (e !== null) etaCache.value.push(e)
|
||||||
|
// }
|
||||||
|
// etaCache.createdAt = Date.now() / 1000
|
||||||
|
// }
|
||||||
|
// return etaCache.value
|
||||||
|
// }
|
88
main.js
88
main.js
|
@ -1,88 +0,0 @@
|
||||||
const express = require('express')
|
|
||||||
const app = express()
|
|
||||||
|
|
||||||
const fetch = require('node-fetch')
|
|
||||||
const conf = require('./config.js')
|
|
||||||
|
|
||||||
const cardinals = ['n', 'e', 's', 'w']
|
|
||||||
const bounds = ['NB', 'EB', 'SB', 'WB']
|
|
||||||
const arrows = ['↑', '->', '↓', '<-']
|
|
||||||
let arrow = (dir) => {
|
|
||||||
let fmt
|
|
||||||
if (dir.length < 2) {
|
|
||||||
fmt = cardinals
|
|
||||||
} else {
|
|
||||||
fmt = bounds
|
|
||||||
}
|
|
||||||
let going = (fmt.indexOf(dir) + cardinals.indexOf(conf.screenDir)) % 4
|
|
||||||
return arrows[going]
|
|
||||||
}
|
|
||||||
|
|
||||||
let getETA = async (route) => {
|
|
||||||
let response = await fetch(
|
|
||||||
`https://svc.metrotransit.org/nextripv2/${route.id}`
|
|
||||||
)
|
|
||||||
let data = await response.json()
|
|
||||||
|
|
||||||
if (data.departures.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
let d = data.departures[0]
|
|
||||||
|
|
||||||
let eta = {}
|
|
||||||
eta.name = route.hasOwnProperty('name') ? route.name : d.route_short_name
|
|
||||||
eta.dir = arrow(route.hasOwnProperty('dir') ? route.dir : d.direction_text)
|
|
||||||
// eta.delta = d.departure_text.toLowerCase()
|
|
||||||
|
|
||||||
let dt = (d.departure_time - Date.now() / 1000) / 60
|
|
||||||
if (dt < 1) {
|
|
||||||
eta.delta = 'Due'
|
|
||||||
} else if (dt < 60) {
|
|
||||||
eta.delta = `${Math.ceil(dt)} minutes`
|
|
||||||
} else {
|
|
||||||
eta.delta = `${Math.floor(dt / 60)}h ${
|
|
||||||
Math.ceil(dt) - Math.floor(dt / 60) * 60
|
|
||||||
}m`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dt <= 1) {
|
|
||||||
eta.class = 'now'
|
|
||||||
} else if (dt <= 3) {
|
|
||||||
eta.class = 'soon'
|
|
||||||
} else {
|
|
||||||
eta.class = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
return eta
|
|
||||||
// return {name: '46', dir: '<-', class: 'soon', delta: '2 min'}
|
|
||||||
}
|
|
||||||
|
|
||||||
let etaCache = {
|
|
||||||
value: null,
|
|
||||||
createdAt: 0,
|
|
||||||
}
|
|
||||||
let getETAsFromCache = async (maxAge) => {
|
|
||||||
if (Date.now() / 1000 - etaCache.createdAt >= maxAge) {
|
|
||||||
etaCache.value = []
|
|
||||||
for (let r of conf.routes) {
|
|
||||||
let e = await getETA(r)
|
|
||||||
if (e !== null) etaCache.value.push(e)
|
|
||||||
}
|
|
||||||
etaCache.createdAt = Date.now() / 1000
|
|
||||||
}
|
|
||||||
return etaCache.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static assets (CSS, images)
|
|
||||||
app.use('/', express.static('static'))
|
|
||||||
|
|
||||||
// API for ETA data
|
|
||||||
app.get('/etas', async (req, res) => {
|
|
||||||
let etas = await getETAsFromCache(29) // cache lives for 29 seconds
|
|
||||||
res.json(etas)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.listen(conf.port, () => {
|
|
||||||
console.log(`Backend server started on port ${conf.port}.`)
|
|
||||||
})
|
|
60
nextrip.go
Normal file
60
nextrip.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// type Stop struct {
|
||||||
|
// stop_id uint
|
||||||
|
// latitude float32
|
||||||
|
// longitude float32
|
||||||
|
// description string
|
||||||
|
// }
|
||||||
|
// type Alert struct {
|
||||||
|
// alert_text string
|
||||||
|
// stop_closed bool
|
||||||
|
// }
|
||||||
|
type Departure struct {
|
||||||
|
// actual bool
|
||||||
|
// trip_id string
|
||||||
|
// stop_id uint
|
||||||
|
DepartureText string `json:"departure_text"`
|
||||||
|
DepartureTime int64 `json:"departure_time"`
|
||||||
|
// description string
|
||||||
|
// route_id uint
|
||||||
|
RouteShortName string `json:"route_short_name"`
|
||||||
|
// direction_id uint8
|
||||||
|
DirectionText string `json:"direction_text"`
|
||||||
|
// schedule_relationship string
|
||||||
|
}
|
||||||
|
type RouteData struct {
|
||||||
|
// alerts []Alert
|
||||||
|
Departures []Departure `json:"departures"`
|
||||||
|
// stops []Stop
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchRouteData(id uint) RouteData {
|
||||||
|
resp, err := http.Get(fmt.Sprintf("https://svc.metrotransit.org/nextripv2/%d", id))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Unable to reach NexTrip API:\n%s", err)
|
||||||
|
return RouteData{}
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error when reading NexTrip API response:\n%s", err)
|
||||||
|
return RouteData{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var data RouteData
|
||||||
|
err = json.Unmarshal(body, &data)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error when parsing NexTrip API data:\n%s", err)
|
||||||
|
return RouteData{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
1725
package-lock.json
generated
1725
package-lock.json
generated
File diff suppressed because it is too large
Load diff
31
package.json
31
package.json
|
@ -1,31 +0,0 @@
|
||||||
{
|
|
||||||
"name": "omnibus",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "show schedule for Metro Transit (Twin Cities) routes",
|
|
||||||
"main": "main.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
|
||||||
"start": "node main.js",
|
|
||||||
"dev": "nodemon main.js"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://git.cyberia.club/reese/omnibus"
|
|
||||||
},
|
|
||||||
"author": "reese sapphire <reese@ovine.xyz>",
|
|
||||||
"license": "MIT",
|
|
||||||
"prettier": {
|
|
||||||
"printWidth": 80,
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": true,
|
|
||||||
"jsxSingleQuote": true,
|
|
||||||
"quoteProps": "consistent"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"express": "^4.18.1",
|
|
||||||
"node-fetch": "^2.6.7"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"nodemon": "^2.0.19"
|
|
||||||
}
|
|
||||||
}
|
|
20
readme.md
20
readme.md
|
@ -1,19 +1,21 @@
|
||||||
```
|
```
|
||||||
__ __
|
----------------------------------------------------
|
||||||
.-----.--------.-----|__| |--.--.--.-----.
|
/` '\
|
||||||
| _ | | | | _ | | |__ --|
|
\ __ __ /
|
||||||
|_____|__|__|__|__|__|__|_____|_____|_____|
|
.-----.--------.-----|__| |--.--.--.-----.
|
||||||
|
| _ | | | | _ | | |__ --|
|
||||||
|
|_____|__|__|__|__|__|__|_____|_____|_____|
|
||||||
|
====================================================
|
||||||
```
|
```
|
||||||
|
|
||||||
lil node webserver that shows ETAs for Metro Transit stops
|
lil go webserver that shows ETAs for Metro Transit stops
|
||||||
|
|
||||||
### usage
|
### usage
|
||||||
|
|
||||||
1. download this repo.
|
1. download this repo.
|
||||||
2. edit `config.js` with your desired values.
|
2. edit `config.go` with your desired values.
|
||||||
3. make sure you have nodejs installed, then download dependencies with `npm ci`.
|
3. build it with `go build`.
|
||||||
4. run it with `node main.js`. you can view the page at http://localhost:8080.
|
4. run the `omnibus` binary. you can view the page at http://localhost:8080.
|
||||||
|
|
||||||
### ok but why?
|
### ok but why?
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
let getETAs = async () => {
|
let getETAs = async () => {
|
||||||
let response = await fetch('/etas')
|
let response = await fetch('/etas')
|
||||||
let data = await response.json()
|
let data = await response.json()
|
||||||
|
console.dir(data)
|
||||||
|
|
||||||
let tbody = document.getElementById('etas')
|
let tbody = document.getElementById('etas')
|
||||||
tbody.innerHTML = ''
|
tbody.innerHTML = ''
|
||||||
|
|
Loading…
Reference in a new issue