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"
|
||||
}
|
||||
}
|
14
readme.md
14
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
|
||||
|
||||
1. download this repo.
|
||||
2. edit `config.js` with your desired values.
|
||||
3. make sure you have nodejs installed, then download dependencies with `npm ci`.
|
||||
4. run it with `node main.js`. you can view the page at http://localhost:8080.
|
||||
2. edit `config.go` with your desired values.
|
||||
3. build it with `go build`.
|
||||
4. run the `omnibus` binary. you can view the page at http://localhost:8080.
|
||||
|
||||
### ok but why?
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
let getETAs = async () => {
|
||||
let response = await fetch('/etas')
|
||||
let data = await response.json()
|
||||
console.dir(data)
|
||||
|
||||
let tbody = document.getElementById('etas')
|
||||
tbody.innerHTML = ''
|
||||
|
|
Loading…
Reference in a new issue