This commit is contained in:
Uthman Fatih 2026-10-10 17:58:14 +01:00
parent 08f3b384c1
commit 8548b106a2
2 changed files with 196 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
cache.gob
prayercal
.env
prayercal.ics

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