The MyAnimeList API inconsistently returns statistics status fields (watching, completed, on_hold, dropped, plan_to_watch) as quoted strings for non-zero values (e.g. "8217") but as bare numbers for zero values (e.g. 0). This caused JSON unmarshal errors for anime with zero counts in any status field. Introduce a FlexString custom type that implements json.Unmarshaler to accept both JSON strings and JSON numbers, always storing the result as a string. The type definition lives in MALTypes.go and the unmarshal logic in MALFunctions.go to keep static types and behavior separate.
164 lines
4.8 KiB
Go
164 lines
4.8 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Unmarshalling accidental numbers received from MAL to strings
|
|
func (f *FlexString) UnmarshalJSON(data []byte) error {
|
|
var s string
|
|
if err := json.Unmarshal(data, &s); err == nil {
|
|
*f = FlexString(s)
|
|
return nil
|
|
}
|
|
var n json.Number
|
|
if err := json.Unmarshal(data, &n); err == nil {
|
|
*f = FlexString(string(n))
|
|
return nil
|
|
}
|
|
return fmt.Errorf("FlexString: invalid value")
|
|
}
|
|
|
|
func MALHelper(method string, malUrl string, body url.Values) (json.RawMessage, string, error) {
|
|
client := &http.Client{}
|
|
|
|
req, err := http.NewRequest(method, malUrl, strings.NewReader(body.Encode()))
|
|
|
|
if err != nil {
|
|
message, _ := json.Marshal(struct {
|
|
Message string `json:"message"`
|
|
}{
|
|
Message: "Failed to create request: " + err.Error(),
|
|
})
|
|
return message, "", fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Add("Authorization", "Bearer "+myAnimeListJwt.AccessToken)
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
message, _ := json.Marshal(struct {
|
|
Message string `json:"message"`
|
|
}{
|
|
Message: "Network error: " + err.Error(),
|
|
})
|
|
return message, "", fmt.Errorf("network error: %w", err)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
return nil, resp.Status, fmt.Errorf("failed to read response: %w", err)
|
|
}
|
|
|
|
if resp.Status == "401 Unauthorized" {
|
|
refreshMyAnimeListAuthorizationToken()
|
|
return MALHelper(method, malUrl, body)
|
|
}
|
|
|
|
if resp.Status != "200 OK" && resp.Status != "201 Created" && resp.Status != "202 Accepted" {
|
|
return respBody, resp.Status, fmt.Errorf("API returned status: %s", resp.Status)
|
|
}
|
|
return respBody, resp.Status, nil
|
|
}
|
|
|
|
func (a *App) GetMyAnimeList(count int) (MALWatchlist, error) {
|
|
limit := strconv.Itoa(count)
|
|
user := a.GetMyAnimeListLoggedInUser()
|
|
malUrl := "https://api.myanimelist.net/v2/users/" + user.Name + "/animelist?fields=list_status&status=watching&limit=" + limit
|
|
|
|
var malList MALWatchlist
|
|
|
|
respBody, resStatus, err := MALHelper("GET", malUrl, nil)
|
|
|
|
if err != nil {
|
|
return malList, fmt.Errorf("failed to get MAL watchlist: %w", err)
|
|
}
|
|
|
|
if resStatus == "200 OK" {
|
|
err := json.Unmarshal(respBody, &malList)
|
|
if err != nil {
|
|
log.Printf("Failed to unmarshal json response, %s\n", err)
|
|
return malList, fmt.Errorf("failed to parse response: %w", err)
|
|
}
|
|
}
|
|
|
|
return malList, nil
|
|
}
|
|
|
|
func (a *App) MyAnimeListUpdate(anime MALAnime, update MALUploadStatus) (MalListStatus, error) {
|
|
if update.NumTimesRewatched >= 1 {
|
|
update.IsRewatching = true
|
|
} else {
|
|
update.IsRewatching = false
|
|
}
|
|
body := url.Values{}
|
|
body.Set("status", update.Status)
|
|
body.Set("is_rewatching", strconv.FormatBool(update.IsRewatching))
|
|
body.Set("score", strconv.Itoa(update.Score))
|
|
body.Set("num_watched_episodes", strconv.Itoa(update.NumWatchedEpisodes))
|
|
body.Set("num_times_rewatched", strconv.Itoa(update.NumTimesRewatched))
|
|
body.Set("comments", update.Comments)
|
|
|
|
malUrl := "https://api.myanimelist.net/v2/anime/" + strconv.Itoa(anime.Id) + "/my_list_status"
|
|
var status MalListStatus
|
|
respBody, respStatus, err := MALHelper("PATCH", malUrl, body)
|
|
|
|
if err != nil {
|
|
return status, fmt.Errorf("failed to update MAL entry: %w", err)
|
|
}
|
|
|
|
if respStatus == "200 OK" {
|
|
err := json.Unmarshal(respBody, &status)
|
|
if err != nil {
|
|
log.Printf("Failed to unmarshal json response, %s\n", err)
|
|
return status, fmt.Errorf("failed to parse response: %w", err)
|
|
}
|
|
}
|
|
return status, nil
|
|
}
|
|
|
|
func (a *App) GetMyAnimeListAnime(id int) (MALAnime, error) {
|
|
malUrl := "https://api.myanimelist.net/v2/anime/" + strconv.Itoa(id) + "?fields=id,title,main_picture,alternative_titles,start_date,end_date,synopsis,mean,rank,popularity,num_list_users,num_scoring_users,nsfw,genres,created_at,updated_at,media_type,status,my_list_status,num_episodes,start_season,broadcast,source,average_episode_duration,rating,pictures,background,related_anime,recommendations,studios,statistics"
|
|
var malAnime MALAnime
|
|
respBody, respStatus, err := MALHelper("GET", malUrl, nil)
|
|
|
|
if err != nil {
|
|
return malAnime, fmt.Errorf("failed to get MAL anime: %w", err)
|
|
}
|
|
|
|
if respStatus == "200 OK" {
|
|
err := json.Unmarshal(respBody, &malAnime)
|
|
if err != nil {
|
|
log.Printf("Failed to unmarshal json response, %s\n", err)
|
|
return malAnime, fmt.Errorf("failed to parse response: %w", err)
|
|
}
|
|
}
|
|
return malAnime, nil
|
|
}
|
|
|
|
func (a *App) DeleteMyAnimeListEntry(id int) (bool, error) {
|
|
malUrl := "https://api.myanimelist.net/v2/anime/" + strconv.Itoa(id) + "/my_list_status"
|
|
_, respStatus, err := MALHelper("DELETE", malUrl, nil)
|
|
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to delete MAL entry: %w", err)
|
|
}
|
|
|
|
if respStatus == "200 OK" {
|
|
return true, nil
|
|
} else {
|
|
return false, fmt.Errorf("delete failed with status: %s", respStatus)
|
|
}
|
|
}
|