commit bb8bbae3fe51cc15dcc2c58c5d9f19c961b4eda5 from: amacleod date: Sat Jan 20 15:50:12 2024 UTC Initial commit commit - /dev/null commit + bb8bbae3fe51cc15dcc2c58c5d9f19c961b4eda5 blob - /dev/null blob + 1c363d2841cdff3fecddc69509795cad4461aabf (mode 644) --- /dev/null +++ LICENCE @@ -0,0 +1,14 @@ +BSD Zero Clause License + +Copyright (c) Alisdair MacLeod + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. blob - /dev/null blob + 5dc0685ed70b0306ad2c3c923f573126d3f038c2 (mode 644) --- /dev/null +++ README.md @@ -0,0 +1,28 @@ +Traffic +======= + +Ever been sat in a traffic jam on the motorway and +wondered how long you're going to be sat there? No? +Me neither since Google Maps/Waze/any live update SatNav +became popular. However whilst sat in the back of +a vehicle without access to one of those services I +decided to see if there were any websites offering +a similar service. And there are! But they are, frankly, +crap. Filled with ads, all trying to load a map and +lots of live update JavaScript. Very unhelpful when you +are on a stretch of motorway with only Edge/2G coverage. + +Angered by this experience I built `traffic`. + +It turns out that in the UK (or England at least), the +Highways Agency publishes an RSS feed of all the latest +scheduled and unscheduled traffic disruption. Unfortunately +this feed is for every motorway all at once. + +`traffic` reads this feed and then breaks it down by the +road name (or, I guess number), creating a static HTML +website. + +Just run this on a cron job every hour or so and never +worry about having a slow web connection when looking +for traffic updates again! \ No newline at end of file blob - /dev/null blob + b44c6a6355b8eede7db97841cf7cc1c84972fc3e (mode 644) --- /dev/null +++ go.mod @@ -0,0 +1,3 @@ +module github.com/admacleod/traffic + +go 1.17 blob - /dev/null blob + 291835be76dd8e7b179c18919887b6e418d91b73 (mode 644) --- /dev/null +++ traffic.go @@ -0,0 +1,153 @@ +// Copyright (c) Alisdair MacLeod +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package main + +import ( + "encoding/xml" + "fmt" + "html/template" + "io" + "log" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" + "time" +) + +const ( + roadTmpl = ` + + +{{.Title}} + +{{range .Entries}}

{{.}}

+{{end -}}` + url = "https://m.highwaysengland.co.uk/feeds/rss/AllEvents.xml" +) + +type entry struct { + EntryTitle string + Link string + Description string + Time time.Time +} + +type rss struct { + Items []item `xml:"channel>item"` +} + +type item struct { + Title string `xml:"title"` + PubDate string `xml:"pubDate"` + Link string `xml:"link"` + Description string `xml:"description"` +} + +func parseFeed(feed []byte) ([]entry, error) { + var f rss + if err := xml.Unmarshal(feed, &f); err != nil { + return nil, fmt.Errorf("unmarshaling rss feed: %w", err) + } + var ret []entry + for _, item := range f.Items { + date, err := time.Parse(time.RFC1123, strings.TrimSpace(item.PubDate)) + if err != nil { + return nil, fmt.Errorf("parsing pubDate: %w", err) + } + ret = append(ret, entry{ + EntryTitle: item.Title, + Link: item.Link, + Description: item.Description, + Time: date, + }) + } + return ret, nil +} + +func main() { + log.SetOutput(os.Stderr) + tmpl := template.Must(template.New("road").Parse(roadTmpl)) + locationRegexp := regexp.MustCompile(`Location : The (.+?) `) + statusRegexp := regexp.MustCompile(`Status : (.+?)\.`) + roadworksRegexp := regexp.MustCompile(`Reason : .*?Roadworks.*?\n`) + client := &http.Client{ + Timeout: 15 * time.Second, + } + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Fatalf("error creating http request: %v\n", err) + } + req.Header.Add("User-Agent", "traffic (https://www.alisdairmacleod.co.uk/blog/projects/traffic.html)") + res, err := client.Do(req) + if err != nil { + log.Fatalf("error getting traffic news: %v\n", err) + return + } + defer func() { + if err := res.Body.Close(); err != nil { + log.Fatalf("error closing request body: %v\n", err) + } + }() + rawFeed, err := io.ReadAll(res.Body) + if err != nil { + log.Fatalf("error reading http request: %v\n", err) + } + entries, err := parseFeed(rawFeed) + if err != nil { + log.Fatalf("error parsing traffic news: %v\n", err) + } + + roads := make(map[string][]template.HTML) + for _, entry := range entries { + status := statusRegexp.FindStringSubmatch(entry.Description) + if status == nil || status[1] != "Currently Active" || roadworksRegexp.MatchString(entry.Description) { + continue + } + + location := locationRegexp.FindStringSubmatch(entry.Description) + if location == nil { + continue + } + roads[location[1]] = append(roads[location[1]], template.HTML(strings.ReplaceAll(entry.Description, "\n", "
"))) + } + + if err := os.RemoveAll("traffic"); err != nil { + log.Fatalf("error deleting directory %q: %v\n", "traffic", err) + } + + if err := os.MkdirAll("traffic", os.ModePerm); err != nil { + log.Fatalf("error creating directory %q: %v\n", "traffic", err) + } + + for road, entries := range roads { + outFile, err := os.Create(filepath.Join("traffic", fmt.Sprintf("%s.html", road))) + if err != nil { + log.Printf("error creating file for %q: %v\n", road, err) + continue + } + if err := tmpl.Execute(outFile, struct { + Title string + Entries []template.HTML + }{ + Title: road, + Entries: entries, + }); err != nil { + log.Printf("error creating HTML for %q: %v\n", road, err) + continue + } + } +}