convert the server to golang #1

Merged
reese merged 4 commits from golang into main 2022-09-09 19:50:01 +00:00
11 changed files with 250 additions and 1887 deletions

2
.gitignore vendored
View file

@ -1 +1 @@
node_modules omnibus

32
config.go Normal file
View 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",
},
}

View file

@ -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
View file

@ -0,0 +1,3 @@
module git.cyberia.club/reese/omnibus
go 1.19

142
main.go Normal file
View 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
View file

@ -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
View 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

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View file

@ -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?

View file

@ -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 = ''