added information being pulled from anilist

This commit is contained in:
John O'Keefe 2024-07-24 09:18:45 -04:00
parent b1880690dc
commit 4e11b218be
6 changed files with 498 additions and 196 deletions

View File

@ -1,89 +1,13 @@
package AniList
package main
import (
"AniTrack"
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v2/pkg/runtime"
"io"
"log"
"net/http"
"net/url"
"os"
"strings"
"sync"
)
type AniListJWT struct {
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
type AniListUser struct {
Data struct {
Viewer struct {
ID int `json:"id"`
Name string `json:"name"`
} `json:"Viewer"`
} `json:"data"`
}
type AniListCurrentUserWatchList struct {
Data struct {
Page struct {
PageInfo struct {
Total int `json:"total"`
PerPage int `json:"perPage"`
CurrentPage int `json:"currentPage"`
LastPage int `json:"lastPage"`
HasNextPage bool `json:"hasNextPage"`
} `json:"pageInfo"`
MediaList []struct {
ID int `json:"id"`
MediaID int `json:"mediaId"`
Media struct {
ID int `json:"id"`
IDMal int `json:"idMal"`
Title struct {
Romaji string `json:"romaji"`
English string `json:"english"`
Native string `json:"native"`
} `json:"title"`
Description string `json:"description"`
CoverImage struct {
Large string `json:"large"`
} `json:"coverImage"`
Season string `json:"season"`
SeasonYear int `json:"seasonYear"`
Episodes int `json:"episodes"`
} `json:"media"`
Status string `json:"status"`
Notes string `json:"notes"`
Progress int `json:"progress"`
Score int `json:"score"`
Repeat int `json:"repeat"`
User struct {
Statistics struct {
Anime struct {
Count int `json:"count"`
Statuses []struct {
Status string `json:"status"`
Count int `json:"count"`
} `json:"statuses"`
} `json:"anime"`
} `json:"statistics"`
} `json:"user"`
} `json:"mediaList"`
} `json:"Page"`
} `json:"data"`
}
var jwt AniListJWT
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))
@ -93,7 +17,7 @@ func AniListQuery(body interface{}, login bool) (json.RawMessage, string) {
if login && (AniListJWT{}) != jwt {
response.Header.Add("Authorization", "Bearer "+jwt.AccessToken)
} else if login {
return nil, "Please login to AniList to make this request"
return nil, "Please login to anilist to make this request"
}
response.Header.Add("Content-Type", "application/json")
response.Header.Add("Accept", "application/json")
@ -108,17 +32,10 @@ func AniListQuery(body interface{}, login bool) (json.RawMessage, string) {
returnedBody, err := io.ReadAll(res.Body)
//var post interface{}
//err = json.Unmarshal(returnedBody, &post)
//if err != nil {
// log.Printf("Failed at unmarshal, %s\n", err)
//}
//
//return post
return returnedBody, ""
}
func (a *main.App) GetAniListItem(aniId int) any {
func (a *App) GetAniListItem(aniId int) any {
type Variables struct {
ID int `json:"id"`
ListType string `json:"listType"`
@ -173,7 +90,7 @@ func (a *main.App) GetAniListItem(aniId int) any {
return post
}
func (a *main.App) AniListSearch(query string) any {
func (a *App) AniListSearch(query string) any {
type Variables struct {
Search string `json:"search"`
ListType string `json:"listType"`
@ -223,127 +140,120 @@ func (a *main.App) AniListSearch(query string) any {
return post
}
var ctxShutdown, cancel = context.WithCancel(context.Background())
func (a *main.App) AniListLogin() {
getAniListCodeUrl := "https://anilist.co/api/v2/oauth/authorize?client_id=" + os.Getenv("ANILIST_APP_ID") + "&redirect_uri=" + os.Getenv("ANILIST_CALLBACK_URI") + "&response_type=code"
runtime.BrowserOpenURL(a.ctx, getAniListCodeUrl)
serverDone := &sync.WaitGroup{}
serverDone.Add(1)
handleAniListCallback(serverDone)
serverDone.Wait()
}
func handleAniListCallback(wg *sync.WaitGroup) {
srv := &http.Server{Addr: ":6734"}
http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
select {
case <-ctxShutdown.Done():
fmt.Println("Shutting down...")
return
default:
}
content := r.FormValue("code")
if content != "" {
jwt = getAniListAuthorizationToken(content)
fmt.Println("Shutting down...")
cancel()
err := srv.Shutdown(context.Background())
if err != nil {
log.Println("server.Shutdown:", err)
}
} else {
_, err := fmt.Fprintf(w, "Getting code failed.")
if err != nil {
return
}
}
})
go func() {
defer wg.Done()
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
fmt.Println("Shutting down...")
}()
}
func getAniListAuthorizationToken(content string) AniListJWT {
apiUrl := "https://anilist.co/api/v2/oauth/token"
resource := "/api/v2/oauth/token"
data := url.Values{}
data.Set("grant_type", "authorization_code")
data.Set("client_id", os.Getenv("ANILIST_APP_ID"))
data.Set("client_secret", os.Getenv("ANILIST_SECRET_TOKEN"))
data.Set("redirect_uri", os.Getenv("ANILIST_CALLBACK_URI"))
data.Set("code", content)
u, _ := url.ParseRequestURI(apiUrl)
u.Path = resource
urlStr := u.String()
response, err := http.NewRequest("POST", urlStr, strings.NewReader(data.Encode()))
if err != nil {
log.Printf("Failed at response, %s\n", err)
}
response.Header.Add("content-type", "application/x-www-form-urlencoded")
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)
var post AniListJWT
err = json.Unmarshal(returnedBody, &post)
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
}
return post
}
func (a *main.App) GetAniListLoggedInUserId() AniListUser {
if (AniListJWT{}) == jwt {
a.AniListLogin()
func (a *App) GetAniListUserWatchingList(page int, perPage int, sort string) AniListCurrentUserWatchList {
var user = a.GetAniListLoggedInUserId()
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 {
Viewer {
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 {
romaji
english
native
}
description
coverImage {
large
}
season
seasonYear
status
episodes
nextAiringEpisode{
airingAt
timeUntilAiring
episode
}
}
status
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,
},
}
user, _ := AniListQuery(body, true)
returnedBody, _ := AniListQuery(body, true)
var post AniListUser
err := json.Unmarshal(user, &post)
var post AniListCurrentUserWatchList
err := json.Unmarshal(returnedBody, &post)
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
}
fmt.Println("UserInfo: ", post)
// 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
return post
}
func (a *main.App) GetAniListUserWatchingList() {
}

View File

@ -1 +1,144 @@
package main
type AniListJWT struct {
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
type AniListUser struct {
Data struct {
Viewer struct {
ID int `json:"id"`
Name string `json:"name"`
} `json:"Viewer"`
} `json:"data"`
}
type AniListCurrentUserWatchList struct {
Data struct {
Page struct {
PageInfo struct {
Total int `json:"total"`
PerPage int `json:"perPage"`
CurrentPage int `json:"currentPage"`
LastPage int `json:"lastPage"`
HasNextPage bool `json:"hasNextPage"`
} `json:"pageInfo"`
MediaList []struct {
ID int `json:"id"`
MediaID int `json:"mediaId"`
UserID int `json:"userId"`
Media struct {
ID int `json:"id"`
IDMal int `json:"idMal"`
Title struct {
Romaji string `json:"romaji"`
English string `json:"english"`
Native string `json:"native"`
} `json:"title"`
Description string `json:"description"`
CoverImage struct {
Large string `json:"large"`
} `json:"coverImage"`
Season string `json:"season"`
SeasonYear int `json:"seasonYear"`
Status string `json:"status"`
Episodes int `json:"episodes"`
NextAiringEpisode struct {
AiringAt int `json:"airingAt"`
TimeUntilAiring int `json:"timeUntilAiring"`
Episode int `json:"episode"`
} `json:"nextAiringEpisode"`
} `json:"media"`
Status string `json:"status"`
Notes string `json:"notes"`
Progress int `json:"progress"`
Score int `json:"score"`
Repeat int `json:"repeat"`
User struct {
ID int `json:"id"`
Name string `json:"name"`
Avatar struct {
Large string `json:"large"`
Medium string `json:"medium"`
} `json:"avatar"`
Statistics struct {
Anime struct {
Count int `json:"count"`
Statuses []struct {
Status string `json:"status"`
Count int `json:"count"`
} `json:"statuses"`
} `json:"anime"`
} `json:"statistics"`
} `json:"user"`
} `json:"mediaList"`
} `json:"Page"`
} `json:"data"`
}
var MediaListSort = struct {
MediaId string
MediaIdDesc string
Score string
ScoreDesc string
Status string
StatusDesc string
Progress string
ProgressDesc string
ProgressVolumes string
ProgressVolumesDesc string
Repeat string
RepeatDesc string
Priority string
PriorityDesc string
StartedOn string
StartedOnDesc string
FinishedOn string
FinishedOnDesc string
AddedTime string
AddedTimeDesc string
UpdatedTime string
UpdatedTimeDesc string
MediaTitleRomaji string
MediaTitleRomajiDesc string
MediaTitleEnglish string
MediaTitleEnglishDesc string
MediaTitleNative string
MediaTitleNativeDesc string
MediaPopularity string
MediaPopularityDesc string
}{
MediaId: "MEDIA_ID",
MediaIdDesc: "MEDIA_ID_DESC",
Score: "SCORE",
ScoreDesc: "SCORE_DESC",
Status: "STATUS",
StatusDesc: "STATUS_DESC",
Progress: "PROGRESS",
ProgressDesc: "PROGRESS_DESC",
ProgressVolumes: "PROGRESS_VOLUMES",
ProgressVolumesDesc: "PROGRESS_VOLUMES_DESC",
Repeat: "REPEAT",
RepeatDesc: "REPEAT_DESC",
Priority: "PRIORITY",
PriorityDesc: "PRIORITY_DESC",
StartedOn: "STARTED_ON",
StartedOnDesc: "STARTED_ON_DESC",
FinishedOn: "FINISHED_ON",
FinishedOnDesc: "FINISHED_ON_DESC",
AddedTime: "ADDED_TIME",
AddedTimeDesc: "ADDED_TIME_DESC",
UpdatedTime: "UPDATED_TIME",
UpdatedTimeDesc: "UPDATED_TIME_DESC",
MediaTitleRomaji: "MEDIA_TITLE_ROMAJI",
MediaTitleRomajiDesc: "MEDIA_TITLE_ROMAJI_DESC",
MediaTitleEnglish: "MEDIA_TITLE_ENGLISH",
MediaTitleEnglishDesc: "MEDIA_TITLE_ENGLISH_DESC",
MediaTitleNative: "MEDIA_TITLE_NATIVE",
MediaTitleNativeDesc: "MEDIA_TITLE_NATIVE_DESC",
MediaPopularity: "MEDIA_POPULARITY",
MediaPopularityDesc: "MEDIA_POPULARITY_DESC",
}

View File

@ -1 +1,170 @@
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/99designs/keyring"
"github.com/wailsapp/wails/v2/pkg/runtime"
"io"
"log"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
)
var jwt AniListJWT
var ring, _ = keyring.Open(keyring.Config{
ServiceName: "AniTrack",
})
var ctxShutdown, cancel = context.WithCancel(context.Background())
func (a *App) AniListLogin() {
if (AniListJWT{}) == jwt {
tokenType, err := ring.Get("anilistTokenType")
expiresIn, err := ring.Get("anilistTokenExpiresIn")
accessToken, err := ring.Get("anilistAccessToken")
refreshToken, err := ring.Get("anilistRefreshToken")
if err != nil {
getAniListCodeUrl := "https://anilist.co/api/v2/oauth/authorize?client_id=" + os.Getenv("ANILIST_APP_ID") + "&redirect_uri=" + os.Getenv("ANILIST_CALLBACK_URI") + "&response_type=code"
runtime.BrowserOpenURL(a.ctx, getAniListCodeUrl)
serverDone := &sync.WaitGroup{}
serverDone.Add(1)
handleAniListCallback(serverDone)
serverDone.Wait()
} else {
jwt.TokenType = string(tokenType.Data)
jwt.AccessToken = string(accessToken.Data)
jwt.RefreshToken = string(refreshToken.Data)
expiresInString := string(expiresIn.Data)
jwt.ExpiresIn, _ = strconv.Atoi(expiresInString)
}
}
}
func handleAniListCallback(wg *sync.WaitGroup) {
srv := &http.Server{Addr: ":6734"}
http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
select {
case <-ctxShutdown.Done():
fmt.Println("Shutting down...")
return
default:
}
content := r.FormValue("code")
if content != "" {
jwt = getAniListAuthorizationToken(content)
_ = ring.Set(keyring.Item{
Key: "anilistTokenType",
Data: []byte(jwt.TokenType),
})
_ = ring.Set(keyring.Item{
Key: "anilistTokenExpiresIn",
Data: []byte(string(jwt.ExpiresIn)),
})
_ = ring.Set(keyring.Item{
Key: "anilistAccessToken",
Data: []byte(jwt.AccessToken),
})
_ = ring.Set(keyring.Item{
Key: "anilistRefreshToken",
Data: []byte(jwt.RefreshToken),
})
fmt.Println("Shutting down...")
cancel()
err := srv.Shutdown(context.Background())
if err != nil {
log.Println("server.Shutdown:", err)
}
} else {
_, err := fmt.Fprintf(w, "Getting code failed.")
if err != nil {
return
}
}
})
go func() {
defer wg.Done()
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
fmt.Println("Shutting down...")
}()
}
func getAniListAuthorizationToken(content string) AniListJWT {
apiUrl := "https://anilist.co/api/v2/oauth/token"
resource := "/api/v2/oauth/token"
data := url.Values{}
data.Set("grant_type", "authorization_code")
data.Set("client_id", os.Getenv("ANILIST_APP_ID"))
data.Set("client_secret", os.Getenv("ANILIST_SECRET_TOKEN"))
data.Set("redirect_uri", os.Getenv("ANILIST_CALLBACK_URI"))
data.Set("code", content)
u, _ := url.ParseRequestURI(apiUrl)
u.Path = resource
urlStr := u.String()
response, err := http.NewRequest("POST", urlStr, strings.NewReader(data.Encode()))
if err != nil {
log.Printf("Failed at response, %s\n", err)
}
response.Header.Add("content-type", "application/x-www-form-urlencoded")
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)
var post AniListJWT
err = json.Unmarshal(returnedBody, &post)
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
}
return post
}
func (a *App) GetAniListLoggedInUserId() AniListUser {
a.AniListLogin()
body := struct {
Query string `json:"query"`
}{
Query: `
query {
Viewer {
id
name
}
}
`,
}
user, _ := AniListQuery(body, true)
var post AniListUser
err := json.Unmarshal(user, &post)
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
}
return post
}

View File

@ -1,5 +1,6 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {main} from '../models';
export function AniListLogin():Promise<void>;
@ -7,4 +8,8 @@ export function AniListSearch(arg1:string):Promise<any>;
export function GetAniListItem(arg1:number):Promise<any>;
export function GetAniListLoggedInUserId():Promise<main.AniListUser>;
export function GetAniListUserWatchingList(arg1:number,arg2:number,arg3:string):Promise<main.AniListCurrentUserWatchList>;
export function Greet(arg1:string):Promise<string>;

View File

@ -14,6 +14,14 @@ export function GetAniListItem(arg1) {
return window['go']['main']['App']['GetAniListItem'](arg1);
}
export function GetAniListLoggedInUserId() {
return window['go']['main']['App']['GetAniListLoggedInUserId']();
}
export function GetAniListUserWatchingList(arg1, arg2, arg3) {
return window['go']['main']['App']['GetAniListUserWatchingList'](arg1, arg2, arg3);
}
export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}

67
frontend/wailsjs/go/models.ts Executable file
View File

@ -0,0 +1,67 @@
export namespace main {
export class AniListCurrentUserWatchList {
// Go type: struct { Page struct { PageInfo struct { Total int "json:\"total\""; PerPage int "json:\"perPage\""; CurrentPage int "json:\"currentPage\""; LastPage int "json:\"lastPage\""; HasNextPage bool "json:\"hasNextPage\"" } "json:\"pageInfo\""; MediaList []struct { ID int "json:\"id\""; MediaID int "json:\"mediaId\""; UserID int "json:\"userId\""; Media struct { ID int "json:\"id\""; IDMal int "json:\"idMal\""; Title struct { Romaji string "json:\"romaji\""; English string "json:\"english\""; Native string "json:\"native\"" } "json:\"title\""; Description string "json:\"description\""; CoverImage struct { Large string "json:\"large\"" } "json:\"coverImage\""; Season string "json:\"season\""; SeasonYear int "json:\"seasonYear\""; Status string "json:\"status\""; Episodes int "json:\"episodes\""; NextAiringEpisode struct { AiringAt int "json:\"airingAt\""; TimeUntilAiring int "json:\"timeUntilAiring\""; Episode int "json:\"episode\"" } "json:\"nextAiringEpisode\"" } "json:\"media\""; Status string "json:\"status\""; Notes string "json:\"notes\""; Progress int "json:\"progress\""; Score int "json:\"score\""; Repeat int "json:\"repeat\""; User struct { ID int "json:\"id\""; Name string "json:\"name\""; Avatar struct { Large string "json:\"large\""; Medium string "json:\"medium\"" } "json:\"avatar\""; Statistics struct { Anime struct { Count int "json:\"count\""; Statuses []struct { Status string "json:\"status\""; Count int "json:\"count\"" } "json:\"statuses\"" } "json:\"anime\"" } "json:\"statistics\"" } "json:\"user\"" } "json:\"mediaList\"" } "json:\"Page\"" }
data: any;
static createFrom(source: any = {}) {
return new AniListCurrentUserWatchList(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.data = this.convertValues(source["data"], Object);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class AniListUser {
// Go type: struct { Viewer struct { ID int "json:\"id\""; Name string "json:\"name\"" } "json:\"Viewer\"" }
data: any;
static createFrom(source: any = {}) {
return new AniListUser(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.data = this.convertValues(source["data"], Object);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
}