Add code
This commit is contained in:
parent
08f3b384c1
commit
8548b106a2
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
cache.gob
|
||||
prayercal
|
||||
.env
|
||||
prayercal.ics
|
||||
192
main.go
Normal file
192
main.go
Normal file
@ -0,0 +1,192 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"encoding/json"
|
||||
"github.com/joho/godotenv"
|
||||
|
||||
"time"
|
||||
"encoding/gob"
|
||||
"bytes"
|
||||
|
||||
"fmt"
|
||||
"sync"
|
||||
_ "time/tzdata"
|
||||
|
||||
ics "github.com/arran4/golang-ical"
|
||||
)
|
||||
|
||||
type prayerTimes struct {
|
||||
FetchedAt int64
|
||||
Times map[string]string
|
||||
}
|
||||
|
||||
func main() {
|
||||
// load the env and check for API key, but its ok if there is no .env as there is (hopefully) env variables in the envrionment itself or whatever
|
||||
_ = godotenv.Load()
|
||||
var mu sync.Mutex
|
||||
|
||||
apiKey := os.Getenv("LPT_API_KEY")
|
||||
if apiKey == "" {
|
||||
log.Fatal("LPT_API_KEY is not set")
|
||||
}
|
||||
url := "https://www.londonprayertimes.com/api/times/?format=json&24hours=true&key=" + apiKey
|
||||
|
||||
// times := getTimes(url, "./cache.gob")
|
||||
// example {1791571473 map[Asr:15:43 Dhuhr:12:53 Fajr:05:41 Isha:19:45 Maghrib:18:24 Sunrise:07:11]}
|
||||
|
||||
loc, err := time.LoadLocation("Europe/London")
|
||||
time.Local = loc
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
http.HandleFunc("/prayercal.ics", func(w http.ResponseWriter, r *http.Request) {
|
||||
mu.Lock()
|
||||
times, err := getTimes(url, "/data/cache.gob")
|
||||
mu.Unlock()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
http.Error(w, "upstream error", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := buildCal(times, loc)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
http.Error(w, "bad prayer data", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/calendar; charset=utf-8")
|
||||
io.WriteString(w, out)
|
||||
})
|
||||
log.Fatal(http.ListenAndServe(":5543", nil))
|
||||
}
|
||||
|
||||
func buildCal(times prayerTimes, loc *time.Location) (string, error) {
|
||||
cal := ics.NewCalendar()
|
||||
cal.SetMethod(ics.MethodPublish)
|
||||
|
||||
for _, name := range prayerNames {
|
||||
start, err := parseToday(times.Times[name], loc)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
|
||||
ev := cal.AddEvent(name + "-" + start.Format("20060102") + "@prayercal")
|
||||
ev.SetCreatedTime(time.Now())
|
||||
ev.SetDtStampTime(time.Now())
|
||||
ev.SetModifiedAt(time.Now())
|
||||
ev.SetStartAt(start)
|
||||
ev.SetEndAt(start.Add(20 * time.Minute))
|
||||
ev.SetSummary(name)
|
||||
ev.SetDescription(name + " prayer time")
|
||||
|
||||
for _, a := range []struct{ trigger, desc string }{
|
||||
{"-PT15M", name + " in 15 minutes"},
|
||||
{"PT0S", name + " time now"},
|
||||
} {
|
||||
alarm := ev.AddAlarm()
|
||||
alarm.SetAction(ics.ActionDisplay)
|
||||
alarm.SetTrigger(a.trigger)
|
||||
alarm.SetDescription(a.desc)
|
||||
}
|
||||
}
|
||||
|
||||
return cal.Serialize(), nil
|
||||
}
|
||||
|
||||
var prayerNames = []string{"Fajr", "Sunrise", "Dhuhr", "Asr", "Maghrib", "Isha"}
|
||||
|
||||
var httpClient = &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
func parseToday(hhmm string, loc *time.Location) (time.Time, error) {
|
||||
t, err := time.Parse("15:04", hhmm)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("bad time %q: %w", hhmm, err)
|
||||
}
|
||||
now := time.Now().In(loc)
|
||||
return time.Date(now.Year(), now.Month(), now.Day(), t.Hour(), t.Minute(), 0, 0, loc), nil
|
||||
}
|
||||
|
||||
func readCache(path string) (prayerTimes, bool) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
log.Println("cache open error:", err)
|
||||
}
|
||||
return prayerTimes{}, false
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var cached prayerTimes
|
||||
if err := gob.NewDecoder(file).Decode(&cached); err != nil {
|
||||
log.Println("cache decode error, refetching:", err)
|
||||
return prayerTimes{}, false
|
||||
}
|
||||
y1, m1, d1 := time.Unix(cached.FetchedAt, 0).Date()
|
||||
y2, m2, d2 := time.Now().Date()
|
||||
return cached, y1 == y2 && m1 == m2 && d1 == d2
|
||||
}
|
||||
|
||||
func getTimes(url, cache string) (prayerTimes, error) {
|
||||
if cached, ok := readCache(cache); ok {
|
||||
log.Println("cache hit")
|
||||
return cached, nil
|
||||
}
|
||||
log.Println("cache miss")
|
||||
|
||||
resp, err := httpClient.Get(url)
|
||||
if err != nil {
|
||||
return prayerTimes{}, fmt.Errorf("fetch: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return prayerTimes{}, fmt.Errorf("fetch: status %s", resp.Status)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return prayerTimes{}, fmt.Errorf("read body: %w", err)
|
||||
}
|
||||
|
||||
var raw map[string]string
|
||||
if err := json.Unmarshal(body, &raw); err != nil {
|
||||
return prayerTimes{}, fmt.Errorf("unmarshal: %w", err)
|
||||
}
|
||||
|
||||
form := prayerTimes{
|
||||
FetchedAt: time.Now().Unix(),
|
||||
Times: map[string]string{
|
||||
"Fajr": raw["fajr"],
|
||||
"Sunrise": raw["sunrise"],
|
||||
"Dhuhr": raw["dhuhr"],
|
||||
"Asr": raw["asr"],
|
||||
"Maghrib": raw["magrib"],
|
||||
"Isha": raw["isha"],
|
||||
},
|
||||
}
|
||||
|
||||
// don't cache a response with missing fields
|
||||
for _, name := range prayerNames {
|
||||
if form.Times[name] == "" {
|
||||
return prayerTimes{}, fmt.Errorf("api response missing %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := gob.NewEncoder(&buf).Encode(form); err != nil {
|
||||
log.Println("cache encode error:", err) // still serve the data
|
||||
return form, nil
|
||||
}
|
||||
if err := os.WriteFile(cache, buf.Bytes(), 0644); err != nil {
|
||||
log.Println("cache write error:", err)
|
||||
}
|
||||
|
||||
return form, nil
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user