Files
seanime-docker/seanime-2.9.10/internal/handlers/mal.go
2025-09-20 14:08:38 +01:00

160 lines
4.0 KiB
Go

package handlers
import (
"errors"
"net/http"
"net/url"
"seanime/internal/api/mal"
"seanime/internal/constants"
"seanime/internal/database/models"
"strconv"
"strings"
"time"
"github.com/goccy/go-json"
"github.com/labstack/echo/v4"
)
type MalAuthResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int32 `json:"expires_in"`
TokenType string `json:"token_type"`
}
// HandleMALAuth
//
// @summary fetches the access and refresh tokens for the given code.
// @desc This is used to authenticate the user with MyAnimeList.
// @desc It will save the info in the database, effectively logging the user in.
// @desc The client should re-fetch the server status after this.
// @route /api/v1/mal/auth [POST]
// @returns handlers.MalAuthResponse
func (h *Handler) HandleMALAuth(c echo.Context) error {
type body struct {
Code string `json:"code"`
State string `json:"state"`
CodeVerifier string `json:"code_verifier"`
}
b := new(body)
if err := c.Bind(b); err != nil {
return h.RespondWithError(c, err)
}
client := &http.Client{}
// Build URL
urlData := url.Values{}
urlData.Set("client_id", constants.MalClientId)
urlData.Set("grant_type", "authorization_code")
urlData.Set("code", b.Code)
urlData.Set("code_verifier", b.CodeVerifier)
encodedData := urlData.Encode()
req, err := http.NewRequest("POST", "https://myanimelist.net/v1/oauth2/token", strings.NewReader(encodedData))
if err != nil {
return h.RespondWithError(c, err)
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Content-Length", strconv.Itoa(len(urlData.Encode())))
// Response
res, err := client.Do(req)
if err != nil {
return h.RespondWithError(c, err)
}
defer res.Body.Close()
ret := MalAuthResponse{}
if err := json.NewDecoder(res.Body).Decode(&ret); err != nil {
return h.RespondWithError(c, err)
}
// Save
malInfo := models.Mal{
BaseModel: models.BaseModel{
ID: 1,
UpdatedAt: time.Now(),
},
Username: "",
AccessToken: ret.AccessToken,
RefreshToken: ret.RefreshToken,
TokenExpiresAt: time.Now().Add(time.Duration(ret.ExpiresIn) * time.Second),
}
_, err = h.App.Database.UpsertMalInfo(&malInfo)
if err != nil {
return h.RespondWithError(c, err)
}
return h.RespondWithData(c, ret)
}
// HandleEditMALListEntryProgress
//
// @summary updates the progress of a MAL list entry.
// @route /api/v1/mal/list-entry/progress [POST]
// @returns bool
func (h *Handler) HandleEditMALListEntryProgress(c echo.Context) error {
type body struct {
MediaId *int `json:"mediaId"`
Progress *int `json:"progress"`
}
b := new(body)
if err := c.Bind(b); err != nil {
return h.RespondWithError(c, err)
}
if b.MediaId == nil || b.Progress == nil {
return h.RespondWithError(c, errors.New("mediaId and progress is required"))
}
// Get MAL info
_malInfo, err := h.App.Database.GetMalInfo()
if err != nil {
return h.RespondWithError(c, err)
}
// Verify MAL auth
malInfo, err := mal.VerifyMALAuth(_malInfo, h.App.Database, h.App.Logger)
if err != nil {
return h.RespondWithError(c, err)
}
// Get MAL Wrapper
malWrapper := mal.NewWrapper(malInfo.AccessToken, h.App.Logger)
// Update MAL list entry
err = malWrapper.UpdateAnimeProgress(&mal.AnimeListProgressParams{
NumEpisodesWatched: b.Progress,
}, *b.MediaId)
if err != nil {
return h.RespondWithError(c, err)
}
h.App.Logger.Debug().Msgf("mal: Updated MAL list entry for mediaId %d", *b.MediaId)
return h.RespondWithData(c, true)
}
// HandleMALLogout
//
// @summary logs the user out of MyAnimeList.
// @desc This will delete the MAL info from the database, effectively logging the user out.
// @desc The client should re-fetch the server status after this.
// @route /api/v1/mal/logout [POST]
// @returns bool
func (h *Handler) HandleMALLogout(c echo.Context) error {
err := h.App.Database.DeleteMalInfo()
if err != nil {
return h.RespondWithError(c, err)
}
return h.RespondWithData(c, true)
}