Skip to content

Commit

Permalink
Add support for .apng and modify the cache
Browse files Browse the repository at this point in the history
  • Loading branch information
wavy-cat committed Nov 8, 2024
1 parent 01b9cb9 commit 0c150e8
Show file tree
Hide file tree
Showing 15 changed files with 363 additions and 82 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.idea
/pkg/petpet/test/output.gif
web
web
/pkg/petpet/test/output.apng
/pkg/petpet/test/output_fast.apng
40 changes: 30 additions & 10 deletions cmd/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import (
"fmt"
"github.com/gorilla/mux"
"github.com/wavy-cat/petpet-go/internal/config"
"github.com/wavy-cat/petpet-go/internal/handler/http/ds"
"github.com/wavy-cat/petpet-go/internal/handler/http/ds_apng"
"github.com/wavy-cat/petpet-go/internal/handler/http/ds_gif"
"github.com/wavy-cat/petpet-go/internal/middleware"
"github.com/wavy-cat/petpet-go/internal/repository"
"github.com/wavy-cat/petpet-go/internal/service"
Expand Down Expand Up @@ -38,15 +39,26 @@ func main() {
}(logger)

// Create a cache object
var cacheObj cache.BytesCache
var cachePNG, cacheGIF cache.BytesCache

switch config.CacheStorage {
case "memory":
cacheObj, err = memory.NewLRUCache(config.CacheMemoryCapacity)
cacheGIF, err = memory.NewLRUCache(config.CacheMemoryCapacity)
if err != nil {
logger.Fatal("Error creating memory cache object", zap.Error(err))
}

cachePNG, err = memory.NewLRUCache(config.CacheMemoryCapacity)
if err != nil {
logger.Fatal("Error creating memory cache object", zap.Error(err))
}
case "fs":
cacheObj, err = fs.NewFileSystemCache(config.CacheFSPath)
cacheGIF, err = fs.NewFileSystemCache(config.CacheFSPath)
if err != nil {
logger.Fatal("Error creating memory cache object", zap.Error(err))
}

cachePNG, err = fs.NewFileSystemCache(config.CacheFSPath)
if err != nil {
logger.Fatal("Error creating memory cache object", zap.Error(err))
}
Expand All @@ -62,17 +74,25 @@ func main() {
providers := map[string]repository.AvatarProvider{
"discord": repository.NewDiscordAvatarProvider(discordBot),
}
gifService := service.NewGIFService(cacheObj, providers, petpet.DefaultConfig, quantizers.HierarhicalQuantizer{})
gifService := service.NewGIFService(cacheGIF, providers, petpet.DefaultConfig, quantizers.HierarhicalQuantizer{})
apngService := service.NewAPngService(cachePNG, providers, petpet.DefaultConfig)

// Set up routing
router := mux.NewRouter()

handle := middleware.Logging{
gifHandle := middleware.Logging{
Logger: logger,
Next: ds_gif.NewHandler(gifService),
}
router.Handle("/ds/{user_id}.gif", &gifHandle).Methods(http.MethodGet)

apngHandle := middleware.Logging{
Logger: logger,
Next: ds.NewHandler(gifService),
Next: ds_apng.NewHandler(apngService),
}
router.Handle("/ds/{user_id}.gif", &handle).Methods(http.MethodGet)
router.Handle("/ds/{user_id}", &handle).Methods(http.MethodGet)
router.Handle("/ds/{user_id}.apng", &apngHandle).Methods(http.MethodGet)

router.Handle("/ds/{user_id}", &gifHandle).Methods(http.MethodGet)

router.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
_, err := w.Write([]byte("Waiting for something to happen?"))
Expand All @@ -92,7 +112,7 @@ func main() {

// Start the server
go func() {
logger.Info("Starting the HTTP server...")
logger.Info("Starting the HTTP server...", zap.String("Address", config.HTTPAddress))
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Fatal("Server failed:", zap.Error(err))
}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ require (

require (
github.com/Nykakin/eigenvalues v0.0.0-20180218201739-8f7f53af5d88 // indirect
github.com/kettek/apng v0.0.0-20220823221153-ff692776a607 // indirect
go.uber.org/multierr v1.10.0 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/kettek/apng v0.0.0-20220823221153-ff692776a607 h1:8tP9cdXzcGX2AvweVVG/lxbI7BSjWbNNUustwJ9dQVA=
github.com/kettek/apng v0.0.0-20220823221153-ff692776a607/go.mod h1:x78/VRQYKuCftMWS0uK5e+F5RJ7S4gSlESRWI0Prl6Q=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down
72 changes: 72 additions & 0 deletions internal/handler/http/ds_apng/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package ds_apng

import (
"context"
"github.com/gorilla/mux"
"github.com/wavy-cat/petpet-go/internal/handler/http/utils"
"github.com/wavy-cat/petpet-go/internal/service"
"github.com/wavy-cat/petpet-go/pkg/answer"
"go.uber.org/zap"
"net/http"
)

type Handler struct {
apngService service.APNGService
}

func NewHandler(apngService service.APNGService) *Handler {
return &Handler{apngService: apngService}
}

func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
logger := r.Context().Value("logger").(*zap.Logger)

// Getting the user ID
userId, ok := mux.Vars(r)["user_id"]
if !ok {
logger.Warn("Failed to get user ID", zap.String("user_id", userId))
if err := answer.RespondWithErrorMessage(w, http.StatusBadRequest, "User ID not sent"); err != nil {
logger.Error("Error sending response", zap.Error(err))
}
return
}

// Getting delay
delay, err := utils.ParseDelay(r.URL.Query().Get("delay"))
if err != nil {
if _, err := answer.RespondHTMLError(w, "Incorrect delay", err.Error()); err != nil {
logger.Error("Error sending response", zap.Error(err))
}
return
}

// Setting caching policies
switch r.URL.Query().Get("no-cache") {
case "true":
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, private")
w.Header().Set("Pragma", "no-cache") // For compatibility with older browsers
w.Header().Set("Expires", "0") // For compatibility with older browsers
default:
w.Header().Set("Cache-Control", "max-age=900")
}

// Calling the service to generate GIF
ctx := context.WithValue(context.Background(), "logger", logger)
gif, err := h.apngService.GetOrGenerateAPNG(ctx, userId, "discord", delay)
if err != nil {
title, description := utils.ParseError(err)
if _, err := answer.RespondHTMLError(w, title, description); err != nil {
logger.Error("Error sending response", zap.Error(err))
}
return
}

// Setting Content-Type
w.Header().Set("Content-Type", "image/apng")

// Returning the result
_, err = w.Write(gif)
if err != nil {
logger.Error("Error sending response", zap.Error(err))
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package ds
package ds_gif

import (
"context"
"github.com/gorilla/mux"
"github.com/wavy-cat/petpet-go/internal/handler/http/utils"
"github.com/wavy-cat/petpet-go/internal/service"
"github.com/wavy-cat/petpet-go/pkg/answer"
"go.uber.org/zap"
Expand Down Expand Up @@ -31,7 +32,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}

// Getting delay
delay, err := parseDelay(r.URL.Query().Get("delay"))
delay, err := utils.ParseDelay(r.URL.Query().Get("delay"))
if err != nil {
if _, err := answer.RespondHTMLError(w, "Incorrect delay", err.Error()); err != nil {
logger.Error("Error sending response", zap.Error(err))
Expand All @@ -53,7 +54,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(context.Background(), "logger", logger)
gif, err := h.gifService.GetOrGenerateGif(ctx, userId, "discord", delay)
if err != nil {
title, description := parseError(err)
title, description := utils.ParseError(err)
if _, err := answer.RespondHTMLError(w, title, description); err != nil {
logger.Error("Error sending response", zap.Error(err))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
package ds
package utils

import (
"strconv"
"strings"
)

func parseDelay(value string) (int, error) {
func ParseDelay(value string) (int, error) {
value = strings.TrimSpace(value)
switch value {
case "":
return 2, nil
return 3, nil
default:
return strconv.Atoi(value)
}
}

func parseError(err error) (string, string) {
func ParseError(err error) (string, string) {
switch {
case strings.Contains(err.Error(), "10013"):
return "Not Found", "User not found"
Expand Down
96 changes: 96 additions & 0 deletions internal/service/apng.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package service

import (
"bytes"
"context"
"errors"
"fmt"
"github.com/wavy-cat/petpet-go/internal/repository"
"github.com/wavy-cat/petpet-go/pkg/cache"
"github.com/wavy-cat/petpet-go/pkg/petpet"
"go.uber.org/zap"
"strings"
)

type APNGService interface {
GetOrGenerateAPNG(ctx context.Context, userId, source string, delay int) ([]byte, error)
}

type apngService struct {
config petpet.Config
cache cache.BytesCache
providers map[string]repository.AvatarProvider
}

func NewAPngService(cache cache.BytesCache, providers map[string]repository.AvatarProvider,
config petpet.Config) APNGService {
return &apngService{
config: config,
cache: cache,
providers: providers,
}
}

func (g apngService) GetOrGenerateAPNG(ctx context.Context, userId, source string, delay int) ([]byte, error) {
if strings.ToLower(userId) == "user_id" {
return nil, errors.New("replace user_id in the URL with real Discord user ID 😉")
}

// Getting the required provider
provider, ok := g.providers[source]
if !ok {
return nil, errors.New("unknown avatar source")
}

// Getting the user's avatar id
avatarId, err := provider.GetAvatarId(ctx, userId)
if err != nil {
return nil, err
}

// We check if the GIF is in the cache and if so, return it.
cacheName := fmt.Sprintf("%s-%d", avatarId, delay)

if g.cache != nil {
cachedGif, err := g.cache.Pull(cacheName)
if err == nil {
return cachedGif, nil
} else if err.Error() != "not exist" {
logger, ok := ctx.Value("logger").(*zap.Logger)
if ok {
logger.Warn("Error when retrieving GIF from cache",
zap.Error(err), zap.String("avatar_id", avatarId))
}
}
}

// Getting the user's avatar
avatarImage, err := provider.GetAvatarImage(ctx, userId)
if err != nil {
return nil, err
}

// Generating a GIF
config := g.config
config.Delay = delay
avatarReader := bytes.NewReader(avatarImage)

var buf bytes.Buffer
defer buf.Reset()
err = petpet.MakeAPNG(avatarReader, &buf, config)
if err != nil {
return nil, err
}

data := buf.Bytes()

// Add a GIF to the cache
if g.cache != nil {
go func() {
_ = g.cache.Push(cacheName, data)
}()
}

// Returning the result
return data, nil
}
17 changes: 9 additions & 8 deletions internal/service/gif.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import (
"bytes"
"context"
"errors"
"fmt"
"github.com/wavy-cat/petpet-go/internal/repository"
"github.com/wavy-cat/petpet-go/pkg/cache"
"github.com/wavy-cat/petpet-go/pkg/petpet"
"go.uber.org/zap"
"io"
"strings"
)

Expand Down Expand Up @@ -51,8 +51,10 @@ func (g gifService) GetOrGenerateGif(ctx context.Context, userId, source string,
}

// We check if the GIF is in the cache and if so, return it.
cacheName := fmt.Sprintf("%s-%d", avatarId, delay)

if g.cache != nil {
cachedGif, err := g.cache.Pull(avatarId)
cachedGif, err := g.cache.Pull(cacheName)
if err == nil {
return cachedGif, nil
} else if err.Error() != "not exist" {
Expand All @@ -75,20 +77,19 @@ func (g gifService) GetOrGenerateGif(ctx context.Context, userId, source string,
config.Delay = delay
avatarReader := bytes.NewReader(avatarImage)

gif, err := petpet.MakeGif(avatarReader, config, g.quantizer)
var buf bytes.Buffer
defer buf.Reset()
err = petpet.MakeGif(avatarReader, &buf, config, g.quantizer)
if err != nil {
return nil, err
}

data, err := io.ReadAll(gif)
if err != nil {
return nil, err
}
data := buf.Bytes()

// Add a GIF to the cache
if g.cache != nil {
go func() {
_ = g.cache.Push(avatarId, data)
_ = g.cache.Push(cacheName, data)
}()
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/petpet/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# petpet

Библиотека для генерации pet-pet гифки.
A library for generating pet-pet gifs.
Loading

0 comments on commit 0c150e8

Please sign in to comment.