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.
391 lines
9.5 KiB
Go
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)
|
|
}
|
|
}
|