diff --git a/http/handler.go b/http/handler.go new file mode 100644 index 0000000..ad70a55 --- /dev/null +++ b/http/handler.go @@ -0,0 +1,90 @@ +package http + +import ( + "context" + "github.com/labstack/echo/v4" + client "github.com/siteworxpro/top-wallpaper/reddit" + "github.com/siteworxpro/top-wallpaper/resize" + "io" + "net/http" + "time" +) + +func Get(c echo.Context) error { + cc := c.(*CustomContext) + + var latestImageVal string + var err error + if cc.redis != nil { + latestImage := cc.redis.Get(context.TODO(), "latestImage") + latestImageVal, err = latestImage.Result() + } + + if err != nil || latestImageVal == "" { + c.Logger().Info("Fetching latest image") + latestImageVal, err = client.GetLatestImage() + if err != nil { + return c.String(http.StatusInternalServerError, "Error fetching latest image") + } + + if cc.redis != nil { + cmd := cc.redis.Set(context.TODO(), "latestImage", latestImageVal, 600*time.Second) + if cmd.Err() != nil { + c.Logger().Warn("could not cache image") + } + } + } else { + c.Logger().Info("Image name fetched from cache") + } + + var imageData string + if cc.redis != nil { + latestImageBin := cc.redis.Get(context.TODO(), "latestImage:bin:"+latestImageVal) + imageData = latestImageBin.Val() + } + + if imageData == "" { + response, err := http.Get(latestImageVal) + if err != nil { + return c.String(http.StatusInternalServerError, "Error fetching image") + } + + if response.StatusCode != http.StatusOK { + return c.String(http.StatusInternalServerError, "Error fetching image") + } + + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(response.Body) + + imageDataBytes, err := io.ReadAll(response.Body) + if err != nil { + return c.String(http.StatusInternalServerError, "Error fetching image") + } + + imageData = string(imageDataBytes) + + imageData, err := resize.Shrink(imageData, 1200, 70) + + if err != nil { + return c.String(http.StatusInternalServerError, "Error resizing image") + } + + go func(data string) { + if cc.redis == nil { + return + } + + _, err = cc.redis.Set(context.TODO(), "latestImage:bin:"+latestImageVal, data, 600*time.Second).Result() + if err != nil { + c.Logger().Warn("could not cache image") + } + }(imageData) + } else { + c.Logger().Info("Image data fetched from cache") + } + + c.Response().Header().Set("Cache-Control", "public, max-age=600") + + return c.Blob(http.StatusOK, "image/jpeg", []byte(imageData)) +} diff --git a/http/middleware.go b/http/middleware.go new file mode 100644 index 0000000..8b87abd --- /dev/null +++ b/http/middleware.go @@ -0,0 +1,59 @@ +package http + +import ( + "context" + "fmt" + "github.com/labstack/echo/v4" + "github.com/redis/go-redis/v9" + "os" + "strconv" +) + +type CustomContext struct { + echo.Context + redis *redis.Client +} + +func GetCustomContext(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + redisUrl := os.Getenv("REDIS_URL") + if redisUrl == "" { + c.Logger().Warn("REDIS_URL not set, skipping redis connection. Use REDIS_URL to cache the image") + + return next(&CustomContext{c, nil}) + } + + redisPort := os.Getenv("REDIS_PORT") + if redisPort == "" { + redisPort = "6379" + } + + redisDb := os.Getenv("REDIS_DB") + if redisDb == "" { + redisDb = "0" + } + + redisDbInt, err := strconv.ParseInt(redisDb, 10, 64) + if err != nil { + c.Logger().Warn("REDIS_DB is not a valid integer, skipping redis connection. Use REDIS_DB to cache the image") + + return next(&CustomContext{}) + } + + cc := &CustomContext{c, redis.NewClient(&redis.Options{ + Addr: fmt.Sprintf("%s:%s", redisUrl, redisPort), + Password: os.Getenv("REDIS_PASSWORD"), + DB: int(redisDbInt), + })} + + cmd := cc.redis.Ping(context.Background()) + + if cmd.Err() != nil { + c.Logger().Warn("could not connect to redis") + + cc.redis = nil + } + + return next(cc) + } +} diff --git a/main.go b/main.go index 7e045eb..e7f37e4 100644 --- a/main.go +++ b/main.go @@ -1,72 +1,15 @@ package main import ( - "context" - "encoding/json" - "fmt" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/labstack/gommon/log" - "github.com/redis/go-redis/v9" - "github.com/siteworxpro/top-wallpaper/resize" - "io" + tphttp "github.com/siteworxpro/top-wallpaper/http" "net/http" "os" - "strconv" "strings" - "time" ) -type redditResponse struct { - Kind string `json:"kind"` - Data struct { - After string `json:"after"` - Dist int `json:"dist"` - ModHash string `json:"modhash"` - GeoFilter string `json:"geo_filter"` - Children []struct { - Kind string `json:"kind"` - Data struct { - ApprovedAtUtc interface{} `json:"approved_at_utc"` - Subreddit string `json:"subreddit"` - SelfText string `json:"selftext"` - Url string `json:"url"` - UrlOverriddenByDest string `json:"url_overridden_by_dest"` - MediaMetadata map[string]struct { - Status string `json:"status"` - Id string `json:"id"` - E string `json:"e"` - M string `json:"m"` - S struct { - U string `json:"u"` - X int `json:"x"` - Y int `json:"y"` - } `json:"s"` - P []struct { - U string `json:"u"` - X int `json:"x"` - Y int `json:"y"` - } `json:"p"` - } `json:"media_metadata"` - Preview struct { - Enabled bool `json:"enabled"` - Images []struct { - Source struct { - Url string `json:"url"` - Width int `json:"width"` - Height int `json:"height"` - } `json:"source"` - } `json:"images"` - } `json:"preview"` - } `json:"data"` - } `json:"children"` - } `json:"data"` -} -type CustomContext struct { - echo.Context - redis *redis.Client -} - func main() { e := echo.New() @@ -74,51 +17,7 @@ func main() { e.HideBanner = true - // Middleware - e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - redisUrl := os.Getenv("REDIS_URL") - if redisUrl == "" { - c.Logger().Warn("REDIS_URL not set, skipping redis connection. Use REDIS_URL to cache the image") - - return next(&CustomContext{c, nil}) - } - - redisPort := os.Getenv("REDIS_PORT") - if redisPort == "" { - redisPort = "6379" - } - - redisDb := os.Getenv("REDIS_DB") - if redisDb == "" { - redisDb = "0" - } - - redisDbInt, err := strconv.ParseInt(redisDb, 10, 64) - if err != nil { - c.Logger().Warn("REDIS_DB is not a valid integer, skipping redis connection. Use REDIS_DB to cache the image") - - return next(&CustomContext{}) - } - - cc := &CustomContext{c, redis.NewClient(&redis.Options{ - Addr: fmt.Sprintf("%s:%s", redisUrl, redisPort), - Password: os.Getenv("REDIS_PASSWORD"), - DB: int(redisDbInt), - })} - - cmd := cc.redis.Ping(context.Background()) - - if cmd.Err() != nil { - c.Logger().Warn("could not connect to redis") - - cc.redis = nil - } - - return next(cc) - } - }) - + e.Use(tphttp.GetCustomContext) e.Use(middleware.Logger()) e.Use(middleware.RequestID()) e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ @@ -131,84 +30,8 @@ func main() { if path == "" { path = "/" } - e.GET(path, func(c echo.Context) error { - cc := c.(*CustomContext) - var latestImageVal string - var err error - if cc.redis != nil { - latestImage := cc.redis.Get(context.TODO(), "latestImage") - latestImageVal, err = latestImage.Result() - } - - if err != nil || latestImageVal == "" { - c.Logger().Info("Fetching latest image") - latestImageVal, err = getLatestImage() - if err != nil { - return c.String(http.StatusInternalServerError, "Error fetching latest image") - } - - if cc.redis != nil { - cmd := cc.redis.Set(context.TODO(), "latestImage", latestImageVal, 600*time.Second) - if cmd.Err() != nil { - c.Logger().Warn("could not cache image") - } - } - } else { - c.Logger().Info("Image name fetched from cache") - } - - var imageData string - if cc.redis != nil { - latestImageBin := cc.redis.Get(context.TODO(), "latestImage:bin:"+latestImageVal) - imageData = latestImageBin.Val() - } - - if imageData == "" { - response, err := http.Get(latestImageVal) - if err != nil { - return c.String(http.StatusInternalServerError, "Error fetching image") - } - - if response.StatusCode != http.StatusOK { - return c.String(http.StatusInternalServerError, "Error fetching image") - } - - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(response.Body) - - imageDataBytes, err := io.ReadAll(response.Body) - if err != nil { - return c.String(http.StatusInternalServerError, "Error fetching image") - } - - imageData = string(imageDataBytes) - - imageData, err := resize.Shrink(imageData, 1200, 70) - - if err != nil { - return c.String(http.StatusInternalServerError, "Error resizing image") - } - - go func(data string) { - if cc.redis == nil { - return - } - - _, err = cc.redis.Set(context.TODO(), "latestImage:bin:"+latestImageVal, data, 600*time.Second).Result() - if err != nil { - c.Logger().Warn("could not cache image") - } - }(imageData) - } else { - c.Logger().Info("Image data fetched from cache") - } - - c.Response().Header().Set("Cache-Control", "public, max-age=600") - - return c.Blob(http.StatusOK, "image/jpeg", []byte(imageData)) - }) + e.GET(path, tphttp.Get) e.Logger.Info("Starting server at path " + path) // start the server @@ -220,36 +43,3 @@ func main() { e.Logger.Fatal(e.Start(":" + port)) } - -func getLatestImage() (string, error) { - - response, err := http.Get("https://www.reddit.com/r/wallpaper/.json") - if err != nil { - return "", err - } - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(response.Body) - - if response.StatusCode != http.StatusOK { - return "", fmt.Errorf("error fetching reddit data: %s", response.Status) - } - - jsonBytes, err := io.ReadAll(response.Body) - - redditData := redditResponse{} - err = json.Unmarshal(jsonBytes, &redditData) - if err != nil { - return "", err - } - - index := 1 - url := redditData.Data.Children[index].Data.UrlOverriddenByDest - - for strings.Contains(url, "gallery") { - index++ - url = redditData.Data.Children[index].Data.UrlOverriddenByDest - } - - return url, nil -} diff --git a/reddit/client.go b/reddit/client.go new file mode 100644 index 0000000..7074e50 --- /dev/null +++ b/reddit/client.go @@ -0,0 +1,88 @@ +package reddit + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +type redditResponse struct { + Kind string `json:"kind"` + Data struct { + After string `json:"after"` + Dist int `json:"dist"` + ModHash string `json:"modhash"` + GeoFilter string `json:"geo_filter"` + Children []struct { + Kind string `json:"kind"` + Data struct { + ApprovedAtUtc interface{} `json:"approved_at_utc"` + Subreddit string `json:"subreddit"` + SelfText string `json:"selftext"` + Url string `json:"url"` + UrlOverriddenByDest string `json:"url_overridden_by_dest"` + MediaMetadata map[string]struct { + Status string `json:"status"` + Id string `json:"id"` + E string `json:"e"` + M string `json:"m"` + S struct { + U string `json:"u"` + X int `json:"x"` + Y int `json:"y"` + } `json:"s"` + P []struct { + U string `json:"u"` + X int `json:"x"` + Y int `json:"y"` + } `json:"p"` + } `json:"media_metadata"` + Preview struct { + Enabled bool `json:"enabled"` + Images []struct { + Source struct { + Url string `json:"url"` + Width int `json:"width"` + Height int `json:"height"` + } `json:"source"` + } `json:"images"` + } `json:"preview"` + } `json:"data"` + } `json:"children"` + } `json:"data"` +} + +func GetLatestImage() (string, error) { + + response, err := http.Get("https://www.reddit.com/r/wallpaper/.json") + if err != nil { + return "", err + } + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(response.Body) + + if response.StatusCode != http.StatusOK { + return "", fmt.Errorf("error fetching reddit data: %s", response.Status) + } + + jsonBytes, err := io.ReadAll(response.Body) + + redditData := redditResponse{} + err = json.Unmarshal(jsonBytes, &redditData) + if err != nil { + return "", err + } + + index := 1 + url := redditData.Data.Children[index].Data.UrlOverriddenByDest + + for strings.Contains(url, "gallery") { + index++ + url = redditData.Data.Children[index].Data.UrlOverriddenByDest + } + + return url, nil +}