penguin/golifehk

@golifehk bot in Telegram

commit 4efd346ce1d6d7874f9eff743a6ea4249f04aae1

author斟酌 鵬兄 <tgckpg@gmail.com>
date2023-12-12T22:12:13Z
subjectUse better caching mechanism for mtr schedules
commit 4efd346ce1d6d7874f9eff743a6ea4249f04aae1
Author: 斟酌 鵬兄 <tgckpg@gmail.com>
Date:   2023-12-12T22:12:13Z

    Use better caching mechanism for mtr schedules
---
 datasources/mtr/bus/busschedule.go | 80 ++++++++++++++++++++++++++++++++++----
 utils/cache.go                     | 67 +++++++++++++++++++++++++++++++
 2 files changed, 140 insertions(+), 7 deletions(-)

diff --git a/datasources/mtr/bus/busschedule.go b/datasources/mtr/bus/busschedule.go
index 76a6f95..7e708fd 100644
--- a/datasources/mtr/bus/busschedule.go
+++ b/datasources/mtr/bus/busschedule.go
@@ -1,11 +1,14 @@
 package bus
 
 import (
+    "fmt"
     "bytes"
     "encoding/json"
     "io"
+    "log"
     "net/http"
     "path/filepath"
+    "time"
 
     "github.com/tgckpg/golifehk/utils"
 )
@@ -33,9 +36,27 @@ type BusStopBuses struct {
     Suspended string `json:"isSuspended"`
 }
 
+type ScheduleStatusTime struct {
+    time.Time
+}
+
 type BusSchedule struct {
     RefreshTime int `json:"appRefreshTimeInSecond,string"`
     BusStops [] BusStopBuses `json:"busStop"`
+    StatusTime ScheduleStatusTime `json:"routeStatusTime"`
+    RemarksTitle string `json:"routeStatusRemarkTitle"`
+    // 0   = OK
+    // 100 = Rate limit ( Unconfirmed. Not in spec )
+    Status int `json:"status,string"`
+}
+
+func (t *ScheduleStatusTime) UnmarshalJSON(b []byte) (err error) {
+    date, err := time.Parse(`"2006\/01\/02 15:04"`, string(b))
+    if err != nil {
+        return err
+    }
+    t.Time = date
+    return
 }
 
 func getSchedule( lang string, routeName string ) ( *BusSchedule, error ) {
@@ -48,7 +69,6 @@ func getSchedule( lang string, routeName string ) ( *BusSchedule, error ) {
     postLang := "en"
     if lang == "zh-Hant" {
         postLang = "zh"
-
     }
 
     QUERY_FUNC := func() ( io.ReadCloser, error ) {
@@ -68,16 +88,62 @@ func getSchedule( lang string, routeName string ) ( *BusSchedule, error ) {
         return resp.Body, nil
     }
 
-    buff, err := utils.CacheStream( CACHE_PATH, QUERY_FUNC, 60 )
+    cs, err := utils.CacheStreamEx( CACHE_PATH, QUERY_FUNC )
     if err != nil {
         return nil, err
     }
 
-    schedules := BusSchedule{}
-    err = json.Unmarshal( buff.Bytes(), &schedules )
-    if err != nil {
-        return nil, err
+    oldSch := BusSchedule{
+        Status: -1,
+    }
+
+    if cs.Local != nil {
+        err = json.Unmarshal( cs.Local.Bytes(), &oldSch )
+        if err != nil {
+            return nil, err
+        }
+    }
+
+    newSch := BusSchedule{
+        Status: -1,
+    }
+
+    for i := 0; i < 3; i ++ {
+
+        if cs.Remote != nil {
+            err = json.Unmarshal( cs.Remote.Bytes(), &newSch )
+            if err != nil {
+                return nil, err
+            }
+        }
+
+        if newSch.Status == 0 {
+            cs.Commit()
+            return &newSch, nil
+        }
+
+        if oldSch.Status == 0 && cs.NotExpired( 60 ) {
+            log.Printf( "Using cache: %s", CACHE_PATH )
+            return &oldSch, nil
+        }
+
+        if oldSch.StatusTime.Time == newSch.StatusTime.Time {
+            log.Printf( "Using cache: %s", CACHE_PATH )
+            return &oldSch, nil
+        }
+
+        // First time + try again i times
+        err = cs.Reload()
+        log.Printf( "Reloading (%d): %s", i, CACHE_PATH )
+        if err != nil {
+            err = fmt.Errorf( "Error retrieving data: %s", err )
+            return nil, err
+        }
+    }
+
+    if newSch.Status != 0 {
+        err = fmt.Errorf( "%s (%d)", newSch.RemarksTitle, newSch.Status )
     }
 
-    return &schedules, nil
+    return &newSch, err
 }
diff --git a/utils/cache.go b/utils/cache.go
index c61c404..a47a95e 100644
--- a/utils/cache.go
+++ b/utils/cache.go
@@ -9,6 +9,73 @@ import (
     "time"
 )
 
+type CacheStreams struct {
+    Local *bytes.Buffer
+    Remote *bytes.Buffer
+    Path string
+    Query func() ( io.ReadCloser, error )
+}
+
+func ( cs *CacheStreams ) Commit() error {
+
+    f, err := os.OpenFile( cs.Path, os.O_CREATE | os.O_WRONLY | os.O_TRUNC, 0644 )
+    if err != nil {
+        return err
+    }
+
+    defer f.Close()
+
+    _, err = io.Copy( f, bytes.NewReader( cs.Remote.Bytes() ) )
+    log.Printf( "Commit: %s", cs.Path )
+    return err
+}
+
+func ( cs *CacheStreams ) Reload() error {
+
+    s, err := cs.Query()
+    if err != nil {
+        return err
+    }
+
+    defer s.Close()
+
+    cs.Remote = bytes.NewBuffer( []byte{} )
+    _, err = io.Copy( cs.Remote, s )
+
+    return err
+}
+
+func ( cs *CacheStreams ) NotExpired( expires time.Duration ) bool {
+
+    cache, err := os.Stat( cs.Path )
+
+    if err == nil {
+        expired := cache.ModTime().Add( expires * 1e9 )
+        return time.Now().Before( expired )
+    }
+
+    return false
+}
+
+func CacheStreamEx( path string, readStream func() ( io.ReadCloser, error ) ) ( *CacheStreams, error ) {
+
+    cs := CacheStreams{}
+    cs.Path = path
+    cs.Query = readStream
+
+    f, err := os.Open( path )
+    if err == nil {
+        defer f.Close()
+        cs.Local = bytes.NewBuffer( []byte{} )
+        _, err = io.Copy( cs.Local, f )
+        if err != nil {
+            return nil, err
+        }
+    }
+
+    return &cs, err
+}
+
 
 func CacheStream( path string, readStream func() ( io.ReadCloser, error ), expires time.Duration ) ( *bytes.Buffer, error ) {