From c8669da106f0f9f864478da78b7adab1f317ad75 Mon Sep 17 00:00:00 2001 From: Tordarus Date: Thu, 3 Feb 2022 17:23:29 +0100 Subject: [PATCH] initial commit --- airingschedule.go | 53 ++++++++++ api.go | 100 ++++++++++++++++++ go.mod | 3 + go.sum | 0 media.go | 98 ++++++++++++++++++ medialist.go | 51 +++++++++ page.go | 45 ++++++++ queries.go | 257 ++++++++++++++++++++++++++++++++++++++++++++++ sub_selections.go | 111 ++++++++++++++++++++ types.go | 214 ++++++++++++++++++++++++++++++++++++++ user.go | 58 +++++++++++ utils.go | 24 +++++ 12 files changed, 1014 insertions(+) create mode 100644 airingschedule.go create mode 100644 api.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 media.go create mode 100644 medialist.go create mode 100644 page.go create mode 100644 queries.go create mode 100644 sub_selections.go create mode 100644 types.go create mode 100644 user.go create mode 100644 utils.go diff --git a/airingschedule.go b/airingschedule.go new file mode 100644 index 0000000..18984d9 --- /dev/null +++ b/airingschedule.go @@ -0,0 +1,53 @@ +package anilist + +func (api *Api) GetAiringSchedule(vars AiringScheduleQuery, onError func(error)) <-chan *AiringSchedule { + resp := responseObj[*page[AiringSchedule]]{} + return requestPaged(api, getAiringScheduleQuery, vars.toMap(), &resp, onError) +} + +const ( + getAiringScheduleQuery = `query ( + $id: Int, + $mediaId: Int, + $episode: Int, + $airingAt: Int, + $notYetAired: Boolean, + $id_in: [Int], + $id_not_in: [Int], + $mediaId_in: [Int], + $mediaId_not_in: [Int], + $episode_in: [Int], + $episode_not_in: [Int], + $episode_greater: Int, + $episode_lesser: Int, + $airingAt_greater: Int, + $airingAt_lesser: Int, + $sort: [AiringSort], + $page: Int + ) + + { + Page (page: $page) { + pageInfo ` + subSelectionPageInfo + ` + + airingSchedules ( + id: $id, + mediaId: $mediaId, + episode: $episode, + airingAt: $airingAt, + notYetAired: $notYetAired, + id_in: $id_in, + id_not_in: $id_not_in, + mediaId_in: $mediaId_in, + mediaId_not_in: $mediaId_not_in, + episode_in: $episode_in, + episode_not_in: $episode_not_in, + episode_greater: $episode_greater, + episode_lesser: $episode_lesser, + airingAt_greater: $airingAt_greater, + airingAt_lesser: $airingAt_lesser, + sort: $sort, + ) ` + subSelectionAiringSchedule + ` + } + }` +) diff --git a/api.go b/api.go new file mode 100644 index 0000000..144c1dd --- /dev/null +++ b/api.go @@ -0,0 +1,100 @@ +package anilist + +import ( + "bytes" + "encoding/json" + "net/http" +) + +type Api struct { + AccessToken string +} + +func NewApi(accessToken string) *Api { + return &Api{AccessToken: accessToken} +} + +type queryObj struct { + Query string `json:"query"` + Vars interface{} `json:"variables"` +} + +type responseObj[T any] struct { + Data T `json:"data"` +} + +func request[T any](api *Api, query string, vars map[string]interface{}, respObj *responseObj[T]) error { + q := &queryObj{ + Query: query, + Vars: vars, + } + + queryData, err := json.Marshal(q) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", "https://graphql.anilist.co/", bytes.NewReader(queryData)) + if err != nil { + return err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", "Bearer "+api.AccessToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + //data, _ := ioutil.ReadAll(resp.Body) + //fmt.Println(string(data)) + + dec := json.NewDecoder(resp.Body) + err = dec.Decode(respObj) + if err != nil { + return err + } + + return nil +} + +func requestPaged[R any](api *Api, query string, vars map[string]interface{}, respObj *responseObj[*page[R]], onError func(error)) <-chan *R { + resp := responseObj[struct { + Page *page[R] `json:"Page"` + }]{} + + out := make(chan *R, 50) + + go func() { + defer close(out) + + vars["page"] = 0 + + for { + if p, ok := vars["page"].(int); ok { + vars["page"] = p + 1 + } + + err := request(api, query, vars, &resp) + if err != nil { + if onError != nil { + onError(err) + } + return + } + + for _, value := range resp.Data.Page.Data() { + value := value + out <- &value + } + + if resp.Data.Page.PageInfo.CurrentPage == resp.Data.Page.PageInfo.LastPage { + return + } + } + }() + + return out +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6f60e86 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.tordarus.net/Tordarus/anilist + +go 1.18 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/media.go b/media.go new file mode 100644 index 0000000..918769e --- /dev/null +++ b/media.go @@ -0,0 +1,98 @@ +package anilist + +func (api *Api) GetMedia(vars MediaQuery, onError func(error)) <-chan *Media { + resp := responseObj[*page[Media]]{} + return requestPaged(api, getMediaQuery, vars.toMap(), &resp, onError) +} + +const ( + getMediaQuery = `query ( + $id: Int, + $startDate: FuzzyDateInt, + $endDate: FuzzyDateInt, + $season: MediaSeason, + $seasonYear: Int, + $type: MediaType, + $format: MediaFormat, + $status: MediaStatus, + $episodes: Int, + $duration: Int, + $chapters: Int, + $volumes: Int, + $isAdult: Boolean, + $genre: String, + $tag: String, + $source: MediaSource, + $search: String, + $id_in: [Int], + $id_not_in: [Int], + $startDate_greater: FuzzyDateInt, + $startDate_lesser: FuzzyDateInt, + $endDate_greater: FuzzyDateInt, + $endDate_lesser: FuzzyDateInt, + $format_in: [MediaFormat], + $format_not_in: [MediaFormat], + $status_in: [MediaStatus], + $status_not_in: [MediaStatus], + $episodes_greater: Int, + $episodes_lesser: Int, + $duration_greater: Int, + $duration_lesser: Int, + $volumes_greater: Int, + $volumes_lesser: Int, + $genre_in: [String], + $genre_not_in: [String], + $tag_in: [String], + $tag_not_in: [String], + $source_in: [MediaSource], + $sort:[MediaSort], + $page: Int + ) + + { + Page (page: $page) { + pageInfo ` + subSelectionPageInfo + ` + media ( + id: $id, + startDate: $startDate, + endDate: $endDate, + season: $season, + seasonYear: $seasonYear, + type: $type, + format: $format, + status: $status, + episodes: $episodes, + duration: $duration, + chapters: $chapters, + volumes: $volumes, + isAdult: $isAdult, + genre: $genre, + tag: $tag, + source: $source, + search: $search, + id_in: $id_in, + id_not_in: $id_not_in, + startDate_greater: $startDate_greater, + startDate_lesser: $startDate_lesser, + endDate_greater: $endDate_greater, + endDate_lesser: $endDate_lesser, + format_in: $format_in, + format_not_in: $format_not_in, + status_in: $status_in, + status_not_in: $status_not_in, + episodes_greater: $episodes_greater, + episodes_lesser: $episodes_lesser, + duration_greater: $duration_greater, + duration_lesser: $duration_lesser, + volumes_greater: $volumes_greater, + volumes_lesser: $volumes_lesser, + genre_in: $genre_in, + genre_not_in: $genre_not_in, + tag_in: $tag_in, + tag_not_in: $tag_not_in, + source_in: $source_in, + sort: $sort + ) ` + subSelectionMedia + ` + } + }` +) diff --git a/medialist.go b/medialist.go new file mode 100644 index 0000000..917dc95 --- /dev/null +++ b/medialist.go @@ -0,0 +1,51 @@ +package anilist + +func (api *Api) GetMediaList(vars MediaListQuery, onError func(error)) <-chan *MediaList { + resp := responseObj[*page[MediaList]]{} + return requestPaged(api, getMediaListQuery, vars.toMap(), &resp, onError) +} + +const ( + getMediaListQuery = `query ( + $id: Int, + $userId: Int, + $userName: String, + $type: MediaType, + $status: MediaListStatus, + $mediaId: Int, + $isFollowing: Boolean, + $notes: String, + $userId_in: [Int], + $status_in: [MediaListStatus], + $status_not_in: [MediaListStatus], + $status_not: MediaListStatus, + $mediaId_in: [Int], + $mediaId_not_in: [Int], + $sort: [MediaListSort], + $page: Int + ) + + { + Page (page: $page) { + pageInfo ` + subSelectionPageInfo + ` + + mediaList ( + id: $id, + userId: $userId, + userName: $userName, + type: $type, + status: $status, + mediaId: $mediaId, + isFollowing: $isFollowing, + notes: $notes, + userId_in: $userId_in, + status_in: $status_in, + status_not_in: $status_not_in, + status_not: $status_not, + mediaId_in: $mediaId_in, + mediaId_not_in: $mediaId_not_in, + sort: $sort + ) ` + subSelectionMediaList + ` + } + }` +) diff --git a/page.go b/page.go new file mode 100644 index 0000000..4103078 --- /dev/null +++ b/page.go @@ -0,0 +1,45 @@ +package anilist + +import ( + "reflect" +) + +type page[T any] struct { + PageInfo *pageInfo `json:"pageInfo"` + Media []Media `json:"media"` + MediaList []MediaList `json:"mediaList"` + Users []User `json:"users"` + AiringSchedules []AiringSchedule `json:"airingSchedules"` +} + +func (p *page[T]) Data() []T { + if reflect.TypeOf(new(T)).Elem() == reflect.TypeOf(new(User)).Elem() { + var data interface{} = p.Users + return data.([]T) + } + + if reflect.TypeOf(new(T)).Elem() == reflect.TypeOf(new(Media)).Elem() { + var data interface{} = p.Media + return data.([]T) + } + + if reflect.TypeOf(new(T)).Elem() == reflect.TypeOf(new(MediaList)).Elem() { + var data interface{} = p.MediaList + return data.([]T) + } + + if reflect.TypeOf(new(T)).Elem() == reflect.TypeOf(new(AiringSchedule)).Elem() { + var data interface{} = p.AiringSchedules + return data.([]T) + } + + panic("generic type not implemented: " + reflect.TypeOf(new(T)).Elem().Name()) +} + +type pageInfo struct { + Total int `json:"total"` + CurrentPage int `json:"currentPage"` + LastPage int `json:"lastPage"` + HasNextPage bool `json:"hasNextPage"` + PerPage int `json:"perPage"` +} diff --git a/queries.go b/queries.go new file mode 100644 index 0000000..f1bc57d --- /dev/null +++ b/queries.go @@ -0,0 +1,257 @@ +package anilist + +import "time" + +type MediaListQuery struct { + ID int + UserID int + UserName string + Type MediaType + Status MediaListStatus + MediaID int + Following bool + Notes string + UserIdIn []int + StatusIn []MediaListStatus + StatusNotIn []MediaListStatus + StatusNot MediaListStatus + MediaIdIn []int + MediaIdNotIn []int + Sort []MediaListSort +} + +func (q *MediaListQuery) toMap() map[string]interface{} { + m := map[string]interface{}{} + addValue2InterfaceMap(m, "id", q.ID) + addValue2InterfaceMap(m, "userId", q.UserID) + addValue2InterfaceMap(m, "userName", q.UserName) + addValue2InterfaceMap(m, "type", q.Type) + addValue2InterfaceMap(m, "status", q.Status) + addValue2InterfaceMap(m, "mediaId", q.MediaID) + addValue2InterfaceMap(m, "isFollowing", q.Following) + addValue2InterfaceMap(m, "notes", q.Notes) + addSlice2InterfaceMap(m, "userId_in", q.UserIdIn) + addSlice2InterfaceMap(m, "status_in", q.StatusIn) + addSlice2InterfaceMap(m, "status_not_in", q.StatusNotIn) + addValue2InterfaceMap(m, "status_not", q.StatusNot) + addSlice2InterfaceMap(m, "mediaId_in", q.MediaIdIn) + addSlice2InterfaceMap(m, "mediaId_not_in", q.MediaIdNotIn) + addSlice2InterfaceMap(m, "sort", q.Sort) + return m +} + +type MediaListSort string + +const ( + MediaListSortMediaId MediaListSort = "MEDIA_ID" + MediaListSortMediaIdDesc MediaListSort = "MEDIA_ID_DESC" + MediaListSortScore MediaListSort = "SCORE" + MediaListSortScoreDesc MediaListSort = "SCORE_DESC" + MediaListSortStatus MediaListSort = "STATUS" + MediaListSortStatusDesc MediaListSort = "STATUS_DESC" + MediaListSortProgress MediaListSort = "PROGRESS" + MediaListSortProgressDesc MediaListSort = "PROGRESS_DESC" + MediaListSortProgressVolumes MediaListSort = "PROGRESS_VOLUMES" + MediaListSortProgressVolumesDesc MediaListSort = "PROGRESS_VOLUMES_DESC" + MediaListSortRepeat MediaListSort = "REPEAT" + MediaListSortRepeatDesc MediaListSort = "REPEAT_DESC" + MediaListSortPriority MediaListSort = "PRIORITY" + MediaListSortPriorityDesc MediaListSort = "PRIORITY_DESC" + MediaListSortStartedOn MediaListSort = "STARTED_ON" + MediaListSortStartedOnDesc MediaListSort = "STARTED_ON_DESC" + MediaListSortFinishedOn MediaListSort = "FINISHED_ON" + MediaListSortFinishedOnDesc MediaListSort = "FINISHED_ON_DESC" + MediaListSortAddedTime MediaListSort = "ADDED_TIME" + MediaListSortAddedTimeDesc MediaListSort = "ADDED_TIME_DESC" + MediaListSortUpdatedTime MediaListSort = "UPDATED_TIME" + MediaListSortUpdatedTimeDesc MediaListSort = "UPDATED_TIME_DESC" + MediaListSortMediaTitleRomaji MediaListSort = "MEDIA_TITLE_ROMAJI" + MediaListSortMediaTitleRomajiDesc MediaListSort = "MEDIA_TITLE_ROMAJI_DESC" + MediaListSortMediaTitleEnglish MediaListSort = "MEDIA_TITLE_ENGLISH" + MediaListSortMediaTitleEnglishDesc MediaListSort = "MEDIA_TITLE_ENGLISH_DESC" + MediaListSortMediaTitleNative MediaListSort = "MEDIA_TITLE_NATIVE" + MediaListSortMediaTitleNativeDesc MediaListSort = "MEDIA_TITLE_NATIVE_DESC" + MediaListSortMediaPopularity MediaListSort = "MEDIA_POPULARITY" + MediaListSortMediaPopularityDesc MediaListSort = "MEDIA_POPULARITY_DESC" +) + +type AiringScheduleQuery struct { + ID int + MediaID int + Episode int + AiringAt time.Time + NotYetAired bool + IdIn []int + IdNotIn []int + MediaIdIn []int + MediaIdNotIn []int + EpisodeIn []int + EpisodeNotIn []int + EpisodeGreater int + EpisodeLesser int + AiringAtGreater time.Time + AiringAtLesser time.Time + Sort []AiringSort +} + +func (q *AiringScheduleQuery) toMap() map[string]interface{} { + m := map[string]interface{}{} + addValue2InterfaceMap(m, "id", q.ID) + addValue2InterfaceMap(m, "mediaId", q.MediaID) + addValue2InterfaceMap(m, "episode", q.Episode) + addValue2InterfaceMap(m, "airingAt", q.AiringAt) + addValue2InterfaceMap(m, "notYetAired", q.NotYetAired) + addSlice2InterfaceMap(m, "id_in", q.IdIn) + addSlice2InterfaceMap(m, "id_not_in", q.IdNotIn) + addSlice2InterfaceMap(m, "mediaId_in", q.MediaIdIn) + addSlice2InterfaceMap(m, "mediaId_not_in", q.MediaIdNotIn) + addSlice2InterfaceMap(m, "episode_in", q.EpisodeIn) + addSlice2InterfaceMap(m, "episode_not_in", q.EpisodeNotIn) + addValue2InterfaceMap(m, "episode_greater", q.EpisodeGreater) + addValue2InterfaceMap(m, "episode_lesser", q.EpisodeLesser) + addValue2InterfaceMap(m, "airingAt_greater", q.AiringAtGreater) + addValue2InterfaceMap(m, "airingAt_lesser", q.AiringAtLesser) + addSlice2InterfaceMap(m, "sort", q.Sort) + return m +} + +type AiringSort string + +const ( + AiringSortId AiringSort = "ID" + AiringSortIdDesc AiringSort = "ID_DESC" + AiringSortMediaId AiringSort = "MEDIA_ID" + AiringSortMediaIdDesc AiringSort = "MEDIA_ID_DESC" + AiringSortTime AiringSort = "TIME" + AiringSortTimeDesc AiringSort = "TIME_DESC" + AiringSortEpisode AiringSort = "EPISODE" + AiringSortEpisodeDesc AiringSort = "EPISODE_DESC" +) + +type MediaQuery struct { + ID int + StartDate time.Time + EndDate time.Time + Season MediaSeason + SeasonYear int + Type MediaType + Format MediaFormat + Status MediaStatus + Episodes int + Duration time.Duration + Chapters int + Volumes int + IsAdult bool + Genre string + Tag string + Source MediaSource + Search string + IdIn []int + IdNotIn []int + StartDateGreater time.Time + StartDateLesser time.Time + EndDateGreater time.Time + EndDateLesser time.Time + FormatIn []MediaFormat + FormatNotIn []MediaFormat + StatusIn []MediaStatus + StatusNotIn []MediaStatus + EpisodesGreater int + EpisodesLesser int + DurationGreater time.Duration + DurationLesser time.Duration + VolumesGreater int + VolumesLesser int + GenreIn []string + GenreNotIn []string + TagIn []string + TagNotIn []string + SourceIn []MediaSource + Sort []MediaSort +} + +func (q *MediaQuery) toMap() map[string]interface{} { + m := map[string]interface{}{} + addValue2InterfaceMap(m, "id", q.ID) + addValue2InterfaceMap(m, "startDate", q.StartDate) + addValue2InterfaceMap(m, "endDate", q.EndDate) + addValue2InterfaceMap(m, "season", q.Season) + addValue2InterfaceMap(m, "seasonYear", q.SeasonYear) + addValue2InterfaceMap(m, "type", q.Type) + addValue2InterfaceMap(m, "format", q.Format) + addValue2InterfaceMap(m, "status", q.Status) + addValue2InterfaceMap(m, "episodes", q.Episodes) + addValue2InterfaceMap(m, "duration", q.Duration.Minutes()) + addValue2InterfaceMap(m, "chapters", q.Chapters) + addValue2InterfaceMap(m, "volumes", q.Volumes) + addValue2InterfaceMap(m, "isAdult", q.IsAdult) + addValue2InterfaceMap(m, "genre", q.Genre) + addValue2InterfaceMap(m, "tag", q.Tag) + addValue2InterfaceMap(m, "source", q.Source) + addValue2InterfaceMap(m, "search", q.Search) + addSlice2InterfaceMap(m, "id_in", q.IdIn) + addSlice2InterfaceMap(m, "id_not_in", q.IdNotIn) + addValue2InterfaceMap(m, "startDateGreater", q.StartDateGreater) + addValue2InterfaceMap(m, "startDateLesser", q.StartDateLesser) + addValue2InterfaceMap(m, "endDateGreater", q.EndDateGreater) + addValue2InterfaceMap(m, "endDateLesser", q.EndDateLesser) + addSlice2InterfaceMap(m, "format_in", q.FormatIn) + addSlice2InterfaceMap(m, "format_not_in", q.FormatNotIn) + addSlice2InterfaceMap(m, "status_in", q.StatusIn) + addSlice2InterfaceMap(m, "status_not_in", q.StatusNotIn) + addValue2InterfaceMap(m, "episodesGreater", q.EpisodesGreater) + addValue2InterfaceMap(m, "episodesLesser", q.EpisodesLesser) + addValue2InterfaceMap(m, "durationGreater", q.DurationGreater.Minutes()) + addValue2InterfaceMap(m, "durationLesser", q.DurationLesser.Minutes()) + addValue2InterfaceMap(m, "volumesGreater", q.VolumesGreater) + addValue2InterfaceMap(m, "volumesLesser", q.VolumesLesser) + addSlice2InterfaceMap(m, "genre_in", q.GenreIn) + addSlice2InterfaceMap(m, "genre_not_in", q.GenreNotIn) + addSlice2InterfaceMap(m, "tag_in", q.TagIn) + addSlice2InterfaceMap(m, "tag_not_in", q.TagNotIn) + addSlice2InterfaceMap(m, "source_in", q.SourceIn) + addSlice2InterfaceMap(m, "sort", q.Sort) + return m +} + +type MediaSort string + +const ( + MediaSortId MediaSort = "ID" + MediaSortIdDesc MediaSort = "ID_DESC" + MediaSortTitleRomaji MediaSort = "TITLE_ROMAJI" + MediaSortTitleRomajiDesc MediaSort = "TITLE_ROMAJI_DESC" + MediaSortTitleEnglish MediaSort = "TITLE_ENGLISH" + MediaSortTitleEnglishDesc MediaSort = "TITLE_ENGLISH_DESC" + MediaSortTitleNative MediaSort = "TITLE_NATIVE" + MediaSortTitleNativeDesc MediaSort = "TITLE_NATIVE_DESC" + MediaSortType MediaSort = "TYPE" + MediaSortTypeDesc MediaSort = "TYPE_DESC" + MediaSortFormat MediaSort = "FORMAT" + MediaSortFormatDesc MediaSort = "FORMAT_DESC" + MediaSortStartDate MediaSort = "START_DATE" + MediaSortStartDateDesc MediaSort = "START_DATE_DESC" + MediaSortEndDate MediaSort = "END_DATE" + MediaSortEndDateDesc MediaSort = "END_DATE_DESC" + MediaSortScore MediaSort = "SCORE" + MediaSortScoreDesc MediaSort = "SCORE_DESC" + MediaSortPopularity MediaSort = "POPULARITY" + MediaSortPopularityDesc MediaSort = "POPULARITY_DESC" + MediaSortTrending MediaSort = "TRENDING" + MediaSortTrendingDesc MediaSort = "TRENDING_DESC" + MediaSortEpisodes MediaSort = "EPISODES" + MediaSortEpisodesDesc MediaSort = "EPISODES_DESC" + MediaSortDuration MediaSort = "DURATION" + MediaSortDurationDesc MediaSort = "DURATION_DESC" + MediaSortStatus MediaSort = "STATUS" + MediaSortStatusDesc MediaSort = "STATUS_DESC" + MediaSortChapters MediaSort = "CHAPTERS" + MediaSortChaptersDesc MediaSort = "CHAPTERS_DESC" + MediaSortVolumes MediaSort = "VOLUMES" + MediaSortVolumesDesc MediaSort = "VOLUMES_DESC" + MediaSortUpdatedAt MediaSort = "UPDATED_AT" + MediaSortUpdatedAtDesc MediaSort = "UPDATED_AT_DESC" + MediaSortSearchMatch MediaSort = "SEARCH_MATCH" + MediaSortFavourites MediaSort = "FAVOURITES" + MediaSortFavouritesDesc MediaSort = "FAVOURITES_DESC" +) diff --git a/sub_selections.go b/sub_selections.go new file mode 100644 index 0000000..6dd6e18 --- /dev/null +++ b/sub_selections.go @@ -0,0 +1,111 @@ +package anilist + +const ( + subSelectionAiringSchedule = `{ + id + mediaId + airingAt + timeUntilAiring + episode + media ` + subSelectionMedia + ` + }` + + subSelectionMediaList = `{ + id + userId + mediaId + status + score + progress + progressVolumes + repeat + priority + private + notes + hiddenFromStatusLists + startedAt ` + subSelectionFuzzyDate + ` + completedAt ` + subSelectionFuzzyDate + ` + updatedAt + createdAt + media ` + subSelectionMedia + ` + user ` + subSelectionUser + ` + }` + + subSelectionUser = `{ + id + name + }` + + subSelectionMedia = `{ + id + title { + romaji + english + native + userPreferred + } + type + format + status + description + startDate ` + subSelectionFuzzyDate + ` + endDate ` + subSelectionFuzzyDate + ` + season + seasonYear + seasonInt + episodes + duration + chapters + volumes + countryOfOrigin + isLicensed + source + hashtag + trailer { + id + site + thumbnail + } + updatedAt + coverImage { + extraLarge + large + medium + color + } + bannerImage + genres + synonyms + averageScore + meanScore + popularity + isLocked + trending + favourites + tags { + id + name + description + category + rank + isGeneralSpoiler + isMediaSpoiler + isAdult + userId + } + }` + + subSelectionFuzzyDate = `{ + year + month + day + }` + + subSelectionPageInfo = `{ + total + currentPage + lastPage + hasNextPage + perPage + }` +) diff --git a/types.go b/types.go new file mode 100644 index 0000000..329addc --- /dev/null +++ b/types.go @@ -0,0 +1,214 @@ +package anilist + +import ( + "time" +) + +type User struct { + ID int `json:"id,omitempty"` + Name string `json:"name,omitempty"` +} + +type Media struct { + ID int `json:"id"` + Title MediaTitle `json:"title"` + Type MediaType `json:"type"` + Format MediaFormat `json:"format"` + Status MediaStatus `json:"status"` + Description string `json:"description"` + StartDate FuzzyDate `json:"startDate"` + EndDate FuzzyDate `json:"endDate"` + Season MediaSeason `json:"season"` + SeasonYear int `json:"seasonYear"` + SeasonInt int `json:"seasonInt"` + Episodes int `json:"episodes"` + Duration Minutes `json:"duration"` + Chapters int `json:"chapters"` + Volumes int `json:"volumes"` + CountryOfOrigin string `json:"countryOfOrigin"` + Licensed bool `json:"isLicensed"` + Source MediaSource `json:"source"` + Hashtag string `json:"hashtag"` + Trailer MediaTrailer `json:"trailer"` + UpdatedAt UnixTime `json:"updatedAt"` + CoverImage MediaCoverImage `json:"coverImage"` + BannerImage string `json:"bannerImage"` + Genres []string `json:"genres"` + Synonyms []string `json:"synonyms"` + AverageScore int `json:"averageScore"` + MeanScore int `json:"meanScore"` + Popularity int `json:"popularity"` + Locked bool `json:"isLocked"` + Trending int `json:"trending"` + Favourites int `json:"favourites"` + Tags []MediaTag `json:"tags"` + //TODO +} + +type MediaTitle struct { + Romaji string `json:"romaji"` + English string `json:"english"` + Native string `json:"native"` + UserPreferred string `json:"userPreferred"` +} + +type MediaType string + +const ( + MediaTypeAnime MediaType = "ANIME" + MediaTypeManga MediaType = "MANGA" +) + +type MediaFormat string + +const ( + MediaFormatTV MediaFormat = "TV" + MediaFormatTVShort MediaFormat = "TV_SHORT" + MediaFormatMovie MediaFormat = "MOVIE" + MediaFormatSpecial MediaFormat = "SPECIAL" + MediaFormatOVA MediaFormat = "OVA" + MediaFormatONA MediaFormat = "ONA" + MediaFormatMusic MediaFormat = "MUSIC" + MediaFormatManga MediaFormat = "MANGA" + MediaFormatNovel MediaFormat = "NOVEL" + MediaFormatOneShot MediaFormat = "ONE_SHOT" +) + +type MediaStatus string + +const ( + MediaStatusFinished MediaStatus = "FINISHED" + MediaStatusReleasing MediaStatus = "RELEASING" + MediaStatusNotYetReleased MediaStatus = "NOT_YET_RELEASED" + MediaStatusCancelled MediaStatus = "CANCELLED" +) + +type MediaSeason string + +const ( + MediaSeasonWinter MediaSeason = "WINTER" + MediaSeasonSpring MediaSeason = "SPRING" + MediaSeasonSummer MediaSeason = "SUMMER" + MediaSeasonFall MediaSeason = "FALL" +) + +type MediaSource string + +const ( + MediaSourceOriginal MediaSource = "ORIGINAL" + MediaSourceManga MediaSource = "MANGA" + MediaSourceLightNovel MediaSource = "LIGHT_NOVEL" + MediaSourceVisualNovel MediaSource = "VISUAL_NOVEL" + MediaSourceVideoGame MediaSource = "VIDEO_GAME" + MediaSourceOther MediaSource = "OTHER" + MediaSourceNovel MediaSource = "NOVEL" + MediaSourceDoujinshi MediaSource = "DOUJINSHI" + MediaSourceAnime MediaSource = "ANIME" + MediaSourceWebNovel MediaSource = "WEB_NOVEL" + MediaSourceLiveAction MediaSource = "LIVE_ACTION" + MediaSourceGame MediaSource = "GAME" + MediaSourceComic MediaSource = "COMIC" + MediaSourceMultimediaProject MediaSource = "MULTIMEDIA_PROJECT" + MediaSourcePictureBook MediaSource = "PICTURE_BOOK" +) + +type MediaTrailer struct { + ID string `json:"id"` + Site string `json:"site"` + Thumbnail string `json:"thumbnail"` +} + +type MediaCoverImage struct { + ExtraLarge string `json:"extraLarge"` + Large string `json:"large"` + Medium string `json:"medium"` + Color string `json:"color"` +} + +type MediaTag struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Category string `json:"category"` + Rank int `json:"rank"` + GeneralSpoiler bool `json:"isGeneralSpoiler"` + MediaSpoiler bool `json:"isMediaSpoiler"` + Adult bool `json:"isAdult"` + UserID int `json:"userId"` +} + +type FuzzyDate struct { + Year int `json:"year"` + Month int `json:"month"` + Day int `json:"day"` +} + +func (d FuzzyDate) Time() time.Time { + return time.Date(d.Year, time.Month(d.Month), d.Day, 0, 0, 0, 0, time.UTC) +} + +type FuzzyDateInt int // YYYYMMDD + +func (d FuzzyDateInt) Time() time.Time { + year, month, day := int(d/10000), int(d/100%100), int(d%100) + return time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) +} + +type UnixTime int64 + +func (t UnixTime) Time() time.Time { + return time.Unix(int64(t), 0) +} + +type Minutes int + +func (d Minutes) Duration() time.Duration { + return time.Duration(d) * time.Minute +} + +type Seconds int + +func (d Seconds) Duration() time.Duration { + return time.Duration(d) * time.Second +} + +type MediaList struct { + ID int `json:"id"` + UserID int `json:"userId"` + MediaID int `json:"mediaId"` + Status MediaListStatus `json:"status"` + Score float64 `json:"score"` + Progress int `json:"progress"` + ProgressVolumes int `json:"progressVolumes"` + Repeat int `json:"repeat"` + Priority int `json:"priority"` + Private bool `json:"private"` + Notes string `json:"notes"` + HiddenFromStatusLists bool `json:"hiddenFromStatusLists"` + StartedAt FuzzyDate `json:"startedAt"` + CompletedAt FuzzyDate `json:"completedAt"` + UpdatedAt UnixTime `json:"updatedAt"` + CreatedAt UnixTime `json:"createdAt"` + Media *Media `json:"media"` + User *User `json:"user"` +} + +type MediaListStatus string + +const ( + MediaListStatusCurrent MediaSource = "CURRENT" + MediaListStatusPlanning MediaSource = "PLANNING" + MediaListStatusCompleted MediaSource = "COMPLETED" + MediaListStatusDropped MediaSource = "DROPPED" + MediaListStatusPaused MediaSource = "PAUSED" + MediaListStatusRepeating MediaSource = "REPEATING" +) + +type AiringSchedule struct { + ID int `json:"id"` + MediaID int `json:"mediaId"` + AiringAt UnixTime `json:"airingAt"` + TimeUntilAiring Seconds `json:"timeUntilAiring"` + Episode int `json:"episode"` + Media *Media `json:"media"` +} diff --git a/user.go b/user.go new file mode 100644 index 0000000..f7b9903 --- /dev/null +++ b/user.go @@ -0,0 +1,58 @@ +package anilist + +func (api *Api) GetUserByName(name string) (*User, error) { + query := `query ($name: String) { + User (name: $name) ` + subSelectionUser + ` + }` + + vars := map[string]interface{}{ + "name": name, + } + + resp := responseObj[struct { + User *User + }]{} + + err := request(api, query, vars, &resp) + if err != nil { + return nil, err + } + + return resp.Data.User, nil +} + +func (api *Api) GetUserByID(id int) (*User, error) { + query := `query ($id: Int) { + User (id: $id) ` + subSelectionUser + ` + }` + + vars := map[string]interface{}{ + "id": id, + } + + resp := responseObj[struct { + User *User + }]{} + + err := request(api, query, vars, &resp) + if err != nil { + return nil, err + } + + return resp.Data.User, nil +} + +func (api *Api) SearchUsers(search string, onError func(error)) <-chan *User { + vars := map[string]interface{}{"search": search} + resp := responseObj[*page[User]]{} + return requestPaged(api, searchUsersQuery, vars, &resp, onError) +} + +const ( + searchUsersQuery = `query ($search: String, $page: Int) { + Page (page: $page) { + pageInfo ` + subSelectionPageInfo + ` + users (search: $search) ` + subSelectionUser + ` + } + }` +) diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..13bda9c --- /dev/null +++ b/utils.go @@ -0,0 +1,24 @@ +package anilist + +import ( + "reflect" + "time" +) + +func addValue2InterfaceMap[K, T comparable](m map[K]interface{}, key K, value T) { + if value != *new(T) { + if reflect.TypeOf(new(T)).Elem() == reflect.TypeOf(new(time.Time)).Elem() { + var t interface{} = value + m[key] = t.(time.Time).Unix() + return + } + + m[key] = value + } +} + +func addSlice2InterfaceMap[K, T comparable](m map[K]interface{}, key K, value []T) { + if value != nil && len(value) > 0 { + m[key] = value + } +}