Files
Anitrack/AniListFunctions.go
T

1066 lines
21 KiB
Go

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
}