package main import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" ) func AniListQuery(body interface{}, login bool) (json.RawMessage, string) { reader, _ := json.Marshal(body) response, err := http.NewRequest("POST", "https://graphql.anilist.co", bytes.NewBuffer(reader)) if err != nil { log.Printf("Failed at response, %s\n", err) } if login && (AniListJWT{}) != aniListJwt { response.Header.Add("Authorization", "Bearer "+aniListJwt.AccessToken) } else if login { return nil, "Please login to AniList to make this request" } response.Header.Add("Content-Type", "application/json") response.Header.Add("Accept", "application/json") client := &http.Client{} res, resErr := client.Do(response) if resErr != nil { log.Printf("Failed at res, %s\n", err) } defer res.Body.Close() returnedBody, err := io.ReadAll(res.Body) if err != nil { return nil, "Could not read the returned body." } return returnedBody, res.Status } func (a *App) GetAniListItem(aniId int, login bool) AniListGetSingleAnime { user := a.GetAniListLoggedInUser() var neededVariables interface{} if login { neededVariables = struct { MediaId int `json:"mediaId"` UserId int `json:"userId"` ListType string `json:"listType"` }{ MediaId: aniId, UserId: user.Data.Viewer.ID, ListType: "ANIME", } } else { neededVariables = struct { MediaId int `json:"mediaId"` ListType string `json:"listType"` }{ MediaId: aniId, ListType: "ANIME", } } body := struct { Query string `json:"query"` Variables interface{} `json:"variables"` }{ Query: ` query($userId: Int, $mediaId: Int, $listType: MediaType) { MediaList(mediaId: $mediaId, userId: $userId, type: $listType) { id mediaId userId media { id idMal title { userPreferred romaji english native } description coverImage { extraLarge large medium color } startDate { year month day } endDate { year month day } bannerImage format season seasonYear status episodes duration countryOfOrigin source synonyms averageScore meanScore popularity trending favourites isFavourite relations { nodes { id title { userPreferred romaji english native } } } nextAiringEpisode { airingAt timeUntilAiring episode } airingSchedule { nodes { id airingAt timeUntilAiring episode mediaId } } genres tags { id name description rank isMediaSpoiler isAdult } isAdult } status startedAt{ year month day } completedAt{ year month day } notes progress score repeat user { id name avatar { large medium } statistics { anime { count statuses { status count } } } } } } `, Variables: neededVariables, } returnedBody, status := AniListQuery(body, login) var post AniListGetSingleAnime if status == "404 Not Found" && !login { return post } if status == "404 Not Found" { post = a.GetAniListItem(aniId, false) } err := json.Unmarshal(returnedBody, &post) if err != nil { log.Printf("Failed at unmarshal, %s\n", err) } if !login { post.Data.MediaList.UserID = user.Data.Viewer.ID post.Data.MediaList.Status = "" post.Data.MediaList.StartedAt.Year = 0 post.Data.MediaList.StartedAt.Month = 0 post.Data.MediaList.StartedAt.Day = 0 post.Data.MediaList.CompletedAt.Year = 0 post.Data.MediaList.CompletedAt.Month = 0 post.Data.MediaList.CompletedAt.Day = 0 post.Data.MediaList.Notes = "" post.Data.MediaList.Progress = 0 post.Data.MediaList.Score = 0 post.Data.MediaList.Repeat = 0 post.Data.MediaList.User.ID = user.Data.Viewer.ID post.Data.MediaList.User.Name = user.Data.Viewer.Name post.Data.MediaList.User.Avatar.Large = user.Data.Viewer.Avatar.Large post.Data.MediaList.User.Avatar.Medium = user.Data.Viewer.Avatar.Medium post.Data.MediaList.User.Statistics.Anime.Count = 0 // This provides an empty array and frees up the memory from the garbage collector post.Data.MediaList.User.Statistics.Anime.Statuses = nil } return post } func (a *App) AniListSearch(query string) (interface{}, error) { type Variables struct { Search string `json:"search"` ListType string `json:"listType"` } body := struct { Query string `json:"query"` Variables Variables `json:"variables"` }{ Query: ` query ($search: String!, $listType: MediaType) { Page (page: 1, perPage: 100) { pageInfo { total currentPage lastPage hasNextPage perPage } media (search: $search, type: $listType) { id idMal title { userPreferred romaji english native } description coverImage { extraLarge large medium color } bannerImage format season seasonYear status episodes duration countryOfOrigin source synonyms averageScore meanScore popularity trending favourites isFavourite relations{ nodes{ id title{ userPreferred romaji english native } } } startDate{ year month day } endDate{ year month day } nextAiringEpisode{ airingAt timeUntilAiring episode } airingSchedule{ nodes{ id airingAt timeUntilAiring episode mediaId } } genres tags{ id name description rank isMediaSpoiler isAdult } isAdult } } } `, Variables: Variables{ Search: query, ListType: "ANIME", }, } returnedBody, status := AniListQuery(body, false) if status != "200 OK" { return nil, fmt.Errorf("API search failed with status: %s", status) } var post interface{} err := json.Unmarshal(returnedBody, &post) if err != nil { log.Printf("Failed at unmarshal, %s\n", err) return nil, fmt.Errorf("Failed to parse search results") } return post, nil } func (a *App) GetAniListUserWatchingList(page int, perPage int, sort string) (AniListCurrentUserWatchList, error) { user := a.GetAniListLoggedInUser() type Variables struct { Page int `json:"page"` PerPage int `json:"perPage"` UserId int `json:"userId"` ListType string `json:"listType"` Status string `json:"status"` Sort string `json:"sort"` } body := struct { Query string `json:"query"` Variables Variables `json:"variables"` }{ Query: ` query ( $page: Int $perPage: Int $userId: Int $listType: MediaType $status: MediaListStatus $sort: [MediaListSort] ) { Page(page: $page, perPage: $perPage) { pageInfo { total perPage currentPage lastPage hasNextPage } mediaList(userId: $userId, type: $listType, status: $status, sort: $sort) { id mediaId userId media { id idMal title { userPreferred romaji english native } description coverImage { extraLarge large medium color } bannerImage format season seasonYear status episodes duration countryOfOrigin source synonyms averageScore meanScore popularity trending favourites isFavourite relations{ nodes{ id title{ userPreferred romaji english native } } } startDate{ year month day } endDate{ year month day } nextAiringEpisode{ airingAt timeUntilAiring episode } airingSchedule{ nodes{ id airingAt timeUntilAiring episode mediaId } } genres tags{ id name description rank isMediaSpoiler isAdult } isAdult } status startedAt { year month day } completedAt { year month day } notes progress score repeat user { id name avatar { large medium } statistics { anime { count statuses { status count } } } } } } } `, Variables: Variables{ Page: page, PerPage: perPage, UserId: user.Data.Viewer.ID, ListType: "ANIME", Status: "CURRENT", Sort: sort, }, } returnedBody, status := AniListQuery(body, true) var badPost struct { Errors []struct { Message string `json:"message"` Status int `json:"status"` Locations []struct { Line int `json:"line"` Column int `json:"column"` } `json:"locations"` } `json:"errors"` Data any `json:"data"` } var post AniListCurrentUserWatchList if status == "200 OK" { err := json.Unmarshal(returnedBody, &post) if err != nil { log.Printf("Failed at unmarshal, %s\n", err) } // Getting the real total, finding the real last page and storing that in the Page info statuses := post.Data.Page.MediaList[0].User.Statistics.Anime.Statuses var total int for _, status := range statuses { if status.Status == "CURRENT" { total = status.Count } } lastPage := total / perPage post.Data.Page.PageInfo.Total = total post.Data.Page.PageInfo.LastPage = lastPage } if status == "403 Forbidden" { err := json.Unmarshal(returnedBody, &badPost) if err != nil { log.Printf("Failed at unmarshal, %s\n", err) return post, fmt.Errorf("API authentication error") } return post, fmt.Errorf("AniList API error: %s", badPost.Errors[0].Message) } if status != "200 OK" { return post, fmt.Errorf("API request failed with status: %s", status) } return post, nil } func (a *App) AniListUpdateEntry(updateBody AniListUpdateVariables) AniListGetSingleAnime { body := struct { Query string `json:"query"` Variables AniListUpdateVariables `json:"variables"` }{ Query: ` mutation ( $mediaId: Int $progress: Int $status: MediaListStatus $score: Float $repeat: Int $notes: String $startedAt: FuzzyDateInput $completedAt: FuzzyDateInput ) { SaveMediaListEntry( mediaId: $mediaId progress: $progress status: $status score: $score repeat: $repeat notes: $notes startedAt: $startedAt completedAt: $completedAt ) { id idMal title { userPreferred romaji english native } description coverImage { extraLarge large medium color } bannerImage format season seasonYear status episodes duration countryOfOrigin source synonyms averageScore meanScore popularity trending favourites isFavourite relations { nodes { id title { userPreferred romaji english native } } } startDate { year month day } endDate { year month day } nextAiringEpisode { airingAt timeUntilAiring episode } airingSchedule { nodes { id airingAt timeUntilAiring episode mediaId } } genres tags { id name description rank isMediaSpoiler isAdult } isAdult } status startedAt { year month day } completedAt { year month day } notes progress score repeat user { id name avatar { large medium } statistics { anime { count statuses { status count } } } } } } `, Variables: updateBody, } returnedBody, _ := AniListQuery(body, true) var returnedJson AniListUpdateReturn err := json.Unmarshal(returnedBody, &returnedJson) if err != nil { log.Printf("Failed at unmarshal, %s\n", err) } var post AniListGetSingleAnime post.Data.MediaList = returnedJson.Data.SaveMediaListEntry return post } func (a *App) AniListDeleteEntry(mediaListId int) DeleteAniListReturn { type Variables = struct { Id int `json:"id"` } body := struct { Query string `json:"query"` Variables Variables `json:"variables"` }{ Query: ` mutation( $id:Int, ){ DeleteMediaListEntry( id:$id, ){ deleted } } `, Variables: Variables{ Id: mediaListId, }, } returnedBody, _ := AniListQuery(body, true) var post DeleteAniListReturn err := json.Unmarshal(returnedBody, &post) if err != nil { log.Printf("Failed at unmarshal, %s\n", err) } return post } func (a *App) AniListBrowse( page int, perPage int, id int, isAdult bool, search string, format []string, status string, countryOfOrigin string, source string, season string, seasonYear int, year string, onList bool, yearLesser int, yearGreater int, episodeLesser int, episodeGreater int, durationLesser int, durationGreater int, chapterLesser int, chapterGreater int, volumeLesser int, volumeGreater int, licensedBy []int, isLicensed bool, genres []string, excludedGenres []string, tags []string, excludedTags []string, minimumTagRank int, sort []string) (AniListCurrentUserWatchList, error) { // user := a.GetAniListLoggedInUser() type Variables struct { Page int `json:"page"` PerPage int `json:"perPage"` Id int `json:"id"` IsAdult bool `json:"isAdult"` Search string `json:"search"` Format []string `json:"format"` Status string `json:"status"` CountryOfOrigin string `json:"countryOfOrigin"` Source string `json:"source"` Season string `json:"season"` SeasonYear int `json:"seasonYear"` Year string `json:"year"` OnList bool `json:"onList"` YearLesser int `json:"yearLesser"` YearGreater int `json:"yearGreater"` EpisodeLesser int `json:"episodeLesser"` EpisodeGreater int `json:"episodeGreater"` DurationLesser int `json:"durationLesser"` DurationGreater int `json:"durationGreater"` ChapterLesser int `json:"chapterLesser"` ChapterGreater int `json:"chapterGreater"` VolumeLesser int `json:"volumeLesser"` VolumeGreater int `json:"volumeGreater"` LicensedBy []int `json:"licensedBy"` IsLicensed bool `json:"isLicensed"` Genres []string `json:"genres"` ExcludedGenres []string `json:"excludedGenres"` Tags []string `json:"tags"` ExcludedTags []string `json:"excludedTags"` MinimumTagRank int `json:"minimumTagRank"` Sort []string `json:"sort"` } body := struct { Query string `json:"query"` Variables Variables `json:"variables"` }{ Query: ` query ( $page: Int = 1 $perPage: Int = 20 $id: Int $isAdult: Boolean = false $search: String $format: [MediaFormat] $status: MediaStatus $countryOfOrigin: CountryCode $source: MediaSource $season: MediaSeason $seasonYear: Int $year: String $onList: Boolean $yearLesser: FuzzyDateInt $yearGreater: FuzzyDateInt $episodeLesser: Int $episodeGreater: Int $durationLesser: Int $durationGreater: Int $chapterLesser: Int $chapterGreater: Int $volumeLesser: Int $volumeGreater: Int $licensedBy: [Int] $isLicensed: Boolean $genres: [String] $excludedGenres: [String] $tags: [String] $excludedTags: [String] $minimumTagRank: Int $sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC] ) { Page(page: $page, perPage: $perPage) { pageInfo { total perPage currentPage lastPage hasNextPage } media( id: $id type: ANIME season: $season format_in: $format status: $status countryOfOrigin: $countryOfOrigin source: $source search: $search onList: $onList seasonYear: $seasonYear startDate_like: $year startDate_lesser: $yearLesser startDate_greater: $yearGreater episodes_lesser: $episodeLesser episodes_greater: $episodeGreater duration_lesser: $durationLesser duration_greater: $durationGreater chapters_lesser: $chapterLesser chapters_greater: $chapterGreater volumes_lesser: $volumeLesser volumes_greater: $volumeGreater licensedById_in: $licensedBy isLicensed: $isLicensed genre_in: $genres genre_not_in: $excludedGenres tag_in: $tags tag_not_in: $excludedTags minimumTagRank: $minimumTagRank sort: $sort isAdult: $isAdult ) { id mediaId userId media { id idMal title { userPreferred romaji english native } description coverImage { extraLarge large medium color } bannerImage format season seasonYear status episodes duration countryOfOrigin source synonyms averageScore meanScore popularity trending favourites isFavourite relations { nodes { id title { userPreferred romaji english native } } } startDate { year month day } endDate { year month day } nextAiringEpisode { airingAt timeUntilAiring episode } airingSchedule { nodes { id airingAt timeUntilAiring episode mediaId } } genres tags { id name description rank isMediaSpoiler isAdult } isAdult } } } `, Variables: Variables{ Page: page, PerPage: perPage, Id: id, IsAdult: isAdult, Search: search, Format: format, Status: status, CountryOfOrigin: countryOfOrigin, Source: source, Season: season, SeasonYear: seasonYear, Year: year, OnList: onList, YearLesser: yearLesser, YearGreater: yearGreater, EpisodeLesser: episodeLesser, EpisodeGreater: episodeGreater, DurationLesser: durationLesser, DurationGreater: durationGreater, ChapterLesser: chapterLesser, ChapterGreater: chapterGreater, VolumeLesser: volumeLesser, VolumeGreater: volumeGreater, LicensedBy: licensedBy, IsLicensed: isLicensed, Genres: genres, ExcludedGenres: excludedGenres, Tags: tags, ExcludedTags: excludedTags, MinimumTagRank: minimumTagRank, Sort: sort, }, } returnedBody, status := AniListQuery(body, true) var badPost struct { Errors []struct { Message string `json:"message"` Status int `json:"status"` Locations []struct { Line int `json:"line"` Column int `json:"column"` } `json:"locations"` } `json:"errors"` Data any `json:"data"` } var post AniListCurrentUserWatchList if status == "200 OK" { err := json.Unmarshal(returnedBody, &post) if err != nil { log.Printf("Failed at unmarshal, %s\n", err) } // Getting the real total, finding the real last page and storing that in the Page info statuses := post.Data.Page.MediaList[0].User.Statistics.Anime.Statuses var total int for _, status := range statuses { if status.Status == "CURRENT" { total = status.Count } } lastPage := total / perPage post.Data.Page.PageInfo.Total = total post.Data.Page.PageInfo.LastPage = lastPage } if status == "403 Forbidden" { err := json.Unmarshal(returnedBody, &badPost) if err != nil { log.Printf("Failed at unmarshal, %s\n", err) return post, fmt.Errorf("API authentication error") } return post, fmt.Errorf("AniList API error: %s", badPost.Errors[0].Message) } if status != "200 OK" { return post, fmt.Errorf("API request failed with status: %s", status) } return post, nil }