commit c8669da106f0f9f864478da78b7adab1f317ad75 Author: Tordarus Date: Thu Feb 3 17:23:29 2022 +0100 initial commit 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 + } +}