Use better caching mechanism for mtr schedules
This commit is contained in:
		@@ -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,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return &schedules, nil
 | 
			
		||||
    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 &newSch, err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 ) {
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user