Files
Anitrack/SimklFunctions.go
John O'Keefe bc497521e7 feat(simkl): add comprehensive error handling
SimklFunctions.go:
- Update SimklHelper to return (json.RawMessage, error)
- Add status code validation (200-299 range)
- Update SimklGetUserWatchlist to return (SimklWatchListType, error)
- Update SimklSyncEpisodes to return (SimklAnime, error)
- Update SimklSyncRating to return (SimklAnime, error)
- Update SimklSyncStatus to return (SimklAnime, error)
- Update SimklSearch to return (SimklAnime, error)
- Update SimklSyncRemove to return (bool, error)
- Add proper error wrapping for all failure scenarios

SimklUserFunctions.go:
- Replace log.Fatalf with log.Printf in server error handling (line 119)
- Replace log.Fatal with log.Printf in JSON marshaling (line 141)

All Simkl API calls now properly propagate errors to frontend.
2026-03-30 20:08:20 -04:00

391 lines
9.5 KiB
Go

package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"reflect"
"slices"
"strconv"
)
var SimklWatchList SimklWatchListType
func SimklHelper(method string, url string, body interface{}) (json.RawMessage, error) {
reader, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal body: %w", err)
}
var req *http.Request
client := &http.Client{}
if body != nil {
req, err = http.NewRequest(method, url, bytes.NewBuffer(reader))
} else {
req, err = http.NewRequest(method, url, nil)
}
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+simklJwt.AccessToken)
req.Header.Add("simkl-api-key", Environment.SIMKL_CLIENT_ID)
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("network error: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return respBody, fmt.Errorf("API returned status: %d", resp.StatusCode)
}
return respBody, nil
}
func (a *App) SimklGetUserWatchlist() (SimklWatchListType, error) {
method := "GET"
url := "https://api.simkl.com/sync/all-items/anime"
respBody, err := SimklHelper(method, url, nil)
if err != nil {
return SimklWatchListType{}, fmt.Errorf("failed to get Simkl watchlist: %w", err)
}
var errCheck struct {
Error string `json:"error"`
Message string `json:"message"`
}
err = json.Unmarshal(respBody, &errCheck)
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
return SimklWatchListType{}, fmt.Errorf("failed to parse error response: %w", err)
}
if errCheck.Error != "" {
a.LogoutSimkl()
return SimklWatchListType{}, fmt.Errorf("Simkl API error: %s", errCheck.Message)
}
var watchlist SimklWatchListType
err = json.Unmarshal(respBody, &watchlist)
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
return SimklWatchListType{}, fmt.Errorf("failed to parse watchlist: %w", err)
}
SimklWatchList = watchlist
return watchlist, nil
}
func (a *App) SimklSyncEpisodes(anime SimklAnime, progress int) (SimklAnime, error) {
var episodes []Episode
var url string
var shows []SimklPostShow
if progress >= anime.WatchedEpisodesCount {
for i := 1; i <= progress; i++ {
episodes = append(episodes, Episode{Number: i})
}
url = "https://api.simkl.com/sync/history"
} else {
for i := anime.WatchedEpisodesCount; i > progress; i-- {
episodes = append(episodes, Episode{Number: i})
}
url = "https://api.simkl.com/sync/history/remove"
}
formattedShow := SimklPostShow{
Title: anime.Show.Title,
Ids: Ids{
Simkl: anime.Show.Ids.Simkl,
Mal: anime.Show.Ids.Mal,
Anilist: anime.Show.Ids.AniList,
},
Episodes: episodes,
}
shows = append(shows, formattedShow)
simklSync := SimklSyncHistoryType{shows}
respBody, err := SimklHelper("POST", url, simklSync)
if err != nil {
log.Printf("Failed to sync episodes: %s\n", err)
return anime, fmt.Errorf("failed to sync episodes: %w", err)
}
var success interface{}
err = json.Unmarshal(respBody, &success)
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
return anime, fmt.Errorf("failed to parse response: %w", err)
}
for i, simklAnime := range SimklWatchList.Anime {
if anime.Show.Ids.Simkl == simklAnime.Show.Ids.Simkl {
SimklWatchList.Anime[i].WatchedEpisodesCount = progress
}
}
anime.WatchedEpisodesCount = progress
WatchListUpdate(anime)
return anime, nil
}
func (a *App) SimklSyncRating(anime SimklAnime, rating int) (SimklAnime, error) {
var url string
showWithRating := ShowWithRating{
Title: anime.Show.Title,
Ids: Ids{
Simkl: anime.Show.Ids.Simkl,
Mal: anime.Show.Ids.Mal,
Anilist: anime.Show.Ids.AniList,
},
Rating: rating,
}
showWithoutRating := ShowWithoutRating{
Title: anime.Show.Title,
Ids: Ids{
Simkl: anime.Show.Ids.Simkl,
Mal: anime.Show.Ids.Mal,
Anilist: anime.Show.Ids.AniList,
},
}
var shows []interface{}
if rating > 0 {
shows = append(shows, showWithRating)
url = "https://api.simkl.com/sync/ratings"
} else {
shows = append(shows, showWithoutRating)
url = "https://api.simkl.com/sync/ratings/remove"
}
simklSync := struct {
Shows []interface{} `json:"shows" ts_type:"shows"`
}{shows}
respBody, err := SimklHelper("POST", url, simklSync)
if err != nil {
log.Printf("Failed to sync rating: %s\n", err)
return anime, fmt.Errorf("failed to sync rating: %w", err)
}
var success interface{}
err = json.Unmarshal(respBody, &success)
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
return anime, fmt.Errorf("failed to parse response: %w", err)
}
for i, simklAnime := range SimklWatchList.Anime {
if anime.Show.Ids.Simkl == simklAnime.Show.Ids.Simkl {
SimklWatchList.Anime[i].UserRating = rating
}
}
anime.UserRating = rating
WatchListUpdate(anime)
return anime, nil
}
func (a *App) SimklSyncStatus(anime SimklAnime, status string) (SimklAnime, error) {
url := "https://api.simkl.com/sync/add-to-list"
show := SimklShowStatus{
Title: anime.Show.Title,
Ids: Ids{
Simkl: anime.Show.Ids.Simkl,
Mal: anime.Show.Ids.Mal,
Anilist: anime.Show.Ids.AniList,
},
To: status,
}
var shows []SimklShowStatus
shows = append(shows, show)
simklSync := struct {
Shows []SimklShowStatus `json:"shows" ts_type:"shows"`
}{shows}
respBody, err := SimklHelper("POST", url, simklSync)
if err != nil {
log.Printf("Failed to sync status: %s\n", err)
return anime, fmt.Errorf("failed to sync status: %w", err)
}
var success interface{}
err = json.Unmarshal(respBody, &success)
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
return anime, fmt.Errorf("failed to parse response: %w", err)
}
for i, simklAnime := range SimklWatchList.Anime {
if anime.Show.Ids.Simkl == simklAnime.Show.Ids.Simkl {
SimklWatchList.Anime[i].Status = status
}
}
anime.Status = status
WatchListUpdate(anime)
return anime, nil
}
func (a *App) SimklSearch(aniListAnime MediaList) (SimklAnime, error) {
var result SimklAnime
if reflect.DeepEqual(SimklWatchList, SimklWatchListType{}) {
fmt.Println("Watchlist empty. Calling...")
watchlist, err := a.SimklGetUserWatchlist()
if err != nil {
log.Printf("Failed to get watchlist: %s\n", err)
return result, fmt.Errorf("failed to load watchlist for search: %w", err)
}
SimklWatchList = watchlist
}
for _, anime := range SimklWatchList.Anime {
id, err := strconv.Atoi(anime.Show.Ids.AniList)
if err != nil {
fmt.Println("AniList ID does not exist on " + anime.Show.Title)
}
if id == aniListAnime.Media.ID {
result = anime
}
}
if reflect.DeepEqual(result, SimklAnime{}) {
var anime SimklSearchType
url := "https://api.simkl.com/search/id?anilist=" + strconv.Itoa(aniListAnime.Media.ID)
respBody, err := SimklHelper("GET", url, nil)
if err != nil {
log.Printf("Failed to search Simkl: %s\n", err)
return result, fmt.Errorf("failed to search Simkl by AniList ID: %w", err)
}
err = json.Unmarshal(respBody, &anime)
if len(anime) == 0 {
url = "https://api.simkl.com/search/id?mal=" + strconv.Itoa(aniListAnime.Media.IDMal)
respBody, err = SimklHelper("GET", url, nil)
if err != nil {
log.Printf("Failed to search Simkl by MAL ID: %s\n", err)
return result, fmt.Errorf("failed to search by MAL ID: %w", err)
}
err = json.Unmarshal(respBody, &anime)
}
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
return result, fmt.Errorf("failed to parse search results: %w", err)
}
if len(anime) == 0 {
return result, nil
}
for _, watchListAnime := range SimklWatchList.Anime {
id := watchListAnime.Show.Ids.Simkl
if id == anime[0].Ids.Simkl {
result = watchListAnime
}
}
if reflect.DeepEqual(result, SimklAnime{}) && len(anime) > 0 {
result.Show.Title = anime[0].Title
result.Show.Poster = anime[0].Poster
result.Show.Ids.Simkl = anime[0].Ids.Simkl
result.Show.Ids.Slug = anime[0].Ids.Slug
}
}
return result, nil
}
func (a *App) SimklSyncRemove(anime SimklAnime) (bool, error) {
url := "https://api.simkl.com/sync/history/remove"
var showArray []SimklShowStatus
singleShow := SimklShowStatus{
Title: anime.Show.Title,
Ids: Ids{
Simkl: anime.Show.Ids.Simkl,
Mal: anime.Show.Ids.Mal,
Anilist: anime.Show.Ids.AniList,
},
}
showArray = append(showArray, singleShow)
show := struct {
Shows []SimklShowStatus `json:"shows"`
}{
Shows: showArray,
}
respBody, err := SimklHelper("POST", url, show)
if err != nil {
log.Printf("Failed to sync remove: %s\n", err)
return false, fmt.Errorf("failed to sync remove: %w", err)
}
var success SimklDeleteType
err = json.Unmarshal(respBody, &success)
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
return false, fmt.Errorf("failed to parse response: %w", err)
}
if success.Deleted.Shows >= 1 {
for i, simklAnime := range SimklWatchList.Anime {
if simklAnime.Show.Ids.Simkl == anime.Show.Ids.Simkl {
SimklWatchList.Anime = slices.Delete(SimklWatchList.Anime, i, i+1)
}
}
return true, nil
}
return false, fmt.Errorf("no shows were deleted")
}
func WatchListUpdate(anime SimklAnime) {
updated := false
for i, simklAnime := range SimklWatchList.Anime {
if simklAnime.Show.Ids.Simkl == anime.Show.Ids.Simkl {
SimklWatchList.Anime[i] = anime
updated = true
}
}
if !updated {
SimklWatchList.Anime = append(SimklWatchList.Anime, anime)
}
}