commit - /dev/null
commit + bb8bbae3fe51cc15dcc2c58c5d9f19c961b4eda5
blob - /dev/null
blob + 1c363d2841cdff3fecddc69509795cad4461aabf (mode 644)
--- /dev/null
+++ LICENCE
+BSD Zero Clause License
+
+Copyright (c) Alisdair MacLeod <copying@alisdairmacleod.co.uk>
+
+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
+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
+module github.com/admacleod/traffic
+
+go 1.17
blob - /dev/null
blob + 291835be76dd8e7b179c18919887b6e418d91b73 (mode 644)
--- /dev/null
+++ traffic.go
+// Copyright (c) Alisdair MacLeod <copying@alisdairmacleod.co.uk>
+//
+// 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 = `<!doctype html>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>{{.Title}}</title>
+<link rel="icon" href="data:,">
+{{range .Entries}}<p>{{.}}</p>
+{{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", "<br>")))
+ }
+
+ 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
+ }
+ }
+}