From 5dc7251be00f9648b336ca85039e61ccbb31c4eb Mon Sep 17 00:00:00 2001 From: Gabe Farrell Date: Wed, 30 Apr 2025 17:48:36 -0400 Subject: [PATCH] feat: add response cache to all requests --- anilist.go | 1 + go.mod | 2 ++ go.sum | 2 ++ helpers.go | 6 +++++ main.go | 14 ++++++++--- mal.go | 1 + middleware.go | 64 +++++++++++++++++++++++++++++++++++++++++++++++---- 7 files changed, 83 insertions(+), 7 deletions(-) diff --git a/anilist.go b/anilist.go index 47017e5..6b67bf8 100644 --- a/anilist.go +++ b/anilist.go @@ -106,6 +106,7 @@ func handleAniListAnimeSearch(idMap *ConcurrentMap, permaSkipIds []string) http. log.Printf("Error writing error response: %v", writeErr) } } else { + w.WriteHeader(http.StatusOK) if _, writeErr := w.Write(search); writeErr != nil { log.Printf("Error writing response: %v", writeErr) } diff --git a/go.mod b/go.mod index 9f52f0a..9897791 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,5 @@ module github.com/gabehf/sonarr-anime-importer go 1.23.0 require github.com/darenliang/jikan-go v1.2.3 + +require github.com/patrickmn/go-cache v2.1.0+incompatible diff --git a/go.sum b/go.sum index 5fab5f8..a03a213 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ github.com/darenliang/jikan-go v1.2.3 h1:Nw6ykJU47QW3rwiIBWHyy1cBNM1Cxsz0AVCdqIN278A= github.com/darenliang/jikan-go v1.2.3/go.mod h1:rv7ksvNqc1b0UK7mf1Uc3swPToJXd9EZQLz5C38jk9Q= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= diff --git a/helpers.go b/helpers.go index e52dfbd..fb23098 100644 --- a/helpers.go +++ b/helpers.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "net/http" "net/url" "strconv" ) @@ -28,3 +30,7 @@ func FullAnimeTitle(title, engtitle string) string { return title } } + +func RequestString(r *http.Request) string { + return fmt.Sprintf("%s %s?%s", r.Method, r.URL.Path, r.URL.RawQuery) +} diff --git a/main.go b/main.go index 06c9403..d9c3ec6 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,8 @@ import ( "strings" "sync" "time" + + "github.com/patrickmn/go-cache" ) type ResponseItem struct { @@ -57,9 +59,15 @@ func main() { if permaSkipAniListStr != "" { log.Printf("Always skipping AniList IDs: %v\n", permaSkipAniListIds) } - buildIdMapMiddleware := newRebuildStaleIdMapMiddleware(idMap) - http.HandleFunc("/v1/mal/anime", loggerMiddleware(buildIdMapMiddleware(handleMalAnimeSearch(idMap, permaSkipMalIds)))) - http.HandleFunc("/v1/anilist/anime", loggerMiddleware(buildIdMapMiddleware(handleAniListAnimeSearch(idMap, permaSkipAniListIds)))) + log.Printf("Preparing cache...") + c := cache.New(10*time.Minute, 15*time.Minute) + middleware := []Middleware{ + loggerMiddleware, + newCacheMiddleware(c), + newRebuildStaleIdMapMiddleware(idMap), + } + http.HandleFunc("/v1/mal/anime", ChainMiddleware(handleMalAnimeSearch(idMap, permaSkipMalIds), middleware...)) + http.HandleFunc("/v1/anilist/anime", ChainMiddleware(handleAniListAnimeSearch(idMap, permaSkipAniListIds), middleware...)) log.Println("Listening on :3333") srv := &http.Server{ diff --git a/mal.go b/mal.go index a19521b..afe170c 100644 --- a/mal.go +++ b/mal.go @@ -21,6 +21,7 @@ func handleMalAnimeSearch(idMap *ConcurrentMap, permaSkipMalIds []string) http.H log.Printf("Error writing error response: %v", writeErr) } } else { + w.WriteHeader(http.StatusOK) if _, writeErr := w.Write([]byte(search)); writeErr != nil { log.Printf("Error writing response: %v", writeErr) } diff --git a/middleware.go b/middleware.go index 7df7231..71365d5 100644 --- a/middleware.go +++ b/middleware.go @@ -1,13 +1,31 @@ package main import ( + "bytes" "log" "net/http" "time" + + "github.com/patrickmn/go-cache" ) -func newRebuildStaleIdMapMiddleware(idMap *ConcurrentMap) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { +// from https://medium.com/@chrisgregory_83433/chaining-middleware-in-go-918cfbc5644d +type Middleware func(http.HandlerFunc) http.HandlerFunc + +func ChainMiddleware(h http.HandlerFunc, m ...Middleware) http.HandlerFunc { + if len(m) < 1 { + return h + } + wrapped := h + // loop in reverse to preserve middleware order + for i := len(m) - 1; i >= 0; i-- { + wrapped = m[i](wrapped) + } + return wrapped +} + +func newRebuildStaleIdMapMiddleware(idMap *ConcurrentMap) func(http.HandlerFunc) http.HandlerFunc { + return func(next http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if time.Since(lastBuiltAnimeIdList) > 24*time.Hour { log.Println("Anime ID association table expired, building new table...") @@ -18,9 +36,47 @@ func newRebuildStaleIdMapMiddleware(idMap *ConcurrentMap) func(http.Handler) htt } } -func loggerMiddleware(next http.Handler) http.HandlerFunc { +func loggerMiddleware(next http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Printf("%s %s?%s", r.Method, r.URL.Path, r.URL.RawQuery) + log.Print(RequestString(r)) next.ServeHTTP(w, r) }) } + +type cacheResponseWriter struct { + http.ResponseWriter + status int + body *bytes.Buffer +} + +func (w *cacheResponseWriter) WriteHeader(statusCode int) { + w.status = statusCode + w.ResponseWriter.WriteHeader(statusCode) +} + +func (w *cacheResponseWriter) Write(b []byte) (int, error) { + w.body.Write(b) // Capture body + return w.ResponseWriter.Write(b) +} + +func newCacheMiddleware(c *cache.Cache) func(http.HandlerFunc) http.HandlerFunc { + return func(next http.HandlerFunc) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + key := RequestString(r) + if cachedResp, found := c.Get(key); found { + log.Println("Responding with cached response") + w.WriteHeader(http.StatusOK) + w.Write(cachedResp.([]byte)) + return + } + crw := &cacheResponseWriter{ + ResponseWriter: w, + body: &bytes.Buffer{}, + } + next.ServeHTTP(crw, r) + if crw.status == http.StatusOK { + c.Set(key, crw.body.Bytes(), cache.DefaultExpiration) + } + }) + } +}