node build fixed
This commit is contained in:
14
seanime-2.9.10/internal/api/anilist/.gqlgenc.yml
Normal file
14
seanime-2.9.10/internal/api/anilist/.gqlgenc.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
model:
|
||||
filename: ./models_gen.go
|
||||
client:
|
||||
filename: ./client_gen.go
|
||||
models:
|
||||
DateTime:
|
||||
model: github.com/99designs/gqlgen/graphql.Time
|
||||
endpoint:
|
||||
url: https://graphql.anilist.co
|
||||
query:
|
||||
- "./queries/*.graphql"
|
||||
generate:
|
||||
clientV2: true
|
||||
clientInterfaceName: "GithubGraphQLClient"
|
||||
407
seanime-2.9.10/internal/api/anilist/client.go
Normal file
407
seanime-2.9.10/internal/api/anilist/client.go
Normal file
@@ -0,0 +1,407 @@
|
||||
package anilist
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"seanime/internal/events"
|
||||
"seanime/internal/util"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/Yamashou/gqlgenc/clientv2"
|
||||
"github.com/Yamashou/gqlgenc/graphqljson"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotAuthenticated is returned when trying to access an Anilist API endpoint that requires authentication,
|
||||
// but the client is not authenticated.
|
||||
ErrNotAuthenticated = errors.New("not authenticated")
|
||||
)
|
||||
|
||||
type AnilistClient interface {
|
||||
IsAuthenticated() bool
|
||||
AnimeCollection(ctx context.Context, userName *string, interceptors ...clientv2.RequestInterceptor) (*AnimeCollection, error)
|
||||
AnimeCollectionWithRelations(ctx context.Context, userName *string, interceptors ...clientv2.RequestInterceptor) (*AnimeCollectionWithRelations, error)
|
||||
BaseAnimeByMalID(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*BaseAnimeByMalID, error)
|
||||
BaseAnimeByID(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*BaseAnimeByID, error)
|
||||
SearchBaseAnimeByIds(ctx context.Context, ids []*int, page *int, perPage *int, status []*MediaStatus, inCollection *bool, sort []*MediaSort, season *MediaSeason, year *int, genre *string, format *MediaFormat, interceptors ...clientv2.RequestInterceptor) (*SearchBaseAnimeByIds, error)
|
||||
CompleteAnimeByID(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*CompleteAnimeByID, error)
|
||||
AnimeDetailsByID(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*AnimeDetailsByID, error)
|
||||
ListAnime(ctx context.Context, page *int, search *string, perPage *int, sort []*MediaSort, status []*MediaStatus, genres []*string, averageScoreGreater *int, season *MediaSeason, seasonYear *int, format *MediaFormat, isAdult *bool, interceptors ...clientv2.RequestInterceptor) (*ListAnime, error)
|
||||
ListRecentAnime(ctx context.Context, page *int, perPage *int, airingAtGreater *int, airingAtLesser *int, notYetAired *bool, interceptors ...clientv2.RequestInterceptor) (*ListRecentAnime, error)
|
||||
UpdateMediaListEntry(ctx context.Context, mediaID *int, status *MediaListStatus, scoreRaw *int, progress *int, startedAt *FuzzyDateInput, completedAt *FuzzyDateInput, interceptors ...clientv2.RequestInterceptor) (*UpdateMediaListEntry, error)
|
||||
UpdateMediaListEntryProgress(ctx context.Context, mediaID *int, progress *int, status *MediaListStatus, interceptors ...clientv2.RequestInterceptor) (*UpdateMediaListEntryProgress, error)
|
||||
UpdateMediaListEntryRepeat(ctx context.Context, mediaID *int, repeat *int, interceptors ...clientv2.RequestInterceptor) (*UpdateMediaListEntryRepeat, error)
|
||||
DeleteEntry(ctx context.Context, mediaListEntryID *int, interceptors ...clientv2.RequestInterceptor) (*DeleteEntry, error)
|
||||
MangaCollection(ctx context.Context, userName *string, interceptors ...clientv2.RequestInterceptor) (*MangaCollection, error)
|
||||
SearchBaseManga(ctx context.Context, page *int, perPage *int, sort []*MediaSort, search *string, status []*MediaStatus, interceptors ...clientv2.RequestInterceptor) (*SearchBaseManga, error)
|
||||
BaseMangaByID(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*BaseMangaByID, error)
|
||||
MangaDetailsByID(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*MangaDetailsByID, error)
|
||||
ListManga(ctx context.Context, page *int, search *string, perPage *int, sort []*MediaSort, status []*MediaStatus, genres []*string, averageScoreGreater *int, startDateGreater *string, startDateLesser *string, format *MediaFormat, countryOfOrigin *string, isAdult *bool, interceptors ...clientv2.RequestInterceptor) (*ListManga, error)
|
||||
ViewerStats(ctx context.Context, interceptors ...clientv2.RequestInterceptor) (*ViewerStats, error)
|
||||
StudioDetails(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*StudioDetails, error)
|
||||
GetViewer(ctx context.Context, interceptors ...clientv2.RequestInterceptor) (*GetViewer, error)
|
||||
AnimeAiringSchedule(ctx context.Context, ids []*int, season *MediaSeason, seasonYear *int, previousSeason *MediaSeason, previousSeasonYear *int, nextSeason *MediaSeason, nextSeasonYear *int, interceptors ...clientv2.RequestInterceptor) (*AnimeAiringSchedule, error)
|
||||
AnimeAiringScheduleRaw(ctx context.Context, ids []*int, interceptors ...clientv2.RequestInterceptor) (*AnimeAiringScheduleRaw, error)
|
||||
}
|
||||
|
||||
type (
|
||||
// AnilistClientImpl is a wrapper around the AniList API client.
|
||||
AnilistClientImpl struct {
|
||||
Client *Client
|
||||
logger *zerolog.Logger
|
||||
token string // The token used for authentication with the AniList API
|
||||
}
|
||||
)
|
||||
|
||||
// NewAnilistClient creates a new AnilistClientImpl with the given token.
|
||||
// The token is used for authorization when making requests to the AniList API.
|
||||
func NewAnilistClient(token string) *AnilistClientImpl {
|
||||
ac := &AnilistClientImpl{
|
||||
token: token,
|
||||
Client: &Client{
|
||||
Client: clientv2.NewClient(http.DefaultClient, "https://graphql.anilist.co", nil,
|
||||
func(ctx context.Context, req *http.Request, gqlInfo *clientv2.GQLRequestInfo, res interface{}, next clientv2.RequestInterceptorFunc) error {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if len(token) > 0 {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
return next(ctx, req, gqlInfo, res)
|
||||
}),
|
||||
},
|
||||
logger: util.NewLogger(),
|
||||
}
|
||||
|
||||
ac.Client.Client.CustomDo = ac.customDoFunc
|
||||
|
||||
return ac
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) IsAuthenticated() bool {
|
||||
if ac.Client == nil || ac.Client.Client == nil {
|
||||
return false
|
||||
}
|
||||
if len(ac.token) == 0 {
|
||||
return false
|
||||
}
|
||||
// If the token is not empty, we are authenticated
|
||||
return true
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// Authenticated
|
||||
////////////////////////////////
|
||||
|
||||
func (ac *AnilistClientImpl) UpdateMediaListEntry(ctx context.Context, mediaID *int, status *MediaListStatus, scoreRaw *int, progress *int, startedAt *FuzzyDateInput, completedAt *FuzzyDateInput, interceptors ...clientv2.RequestInterceptor) (*UpdateMediaListEntry, error) {
|
||||
if !ac.IsAuthenticated() {
|
||||
return nil, ErrNotAuthenticated
|
||||
}
|
||||
ac.logger.Debug().Int("mediaId", *mediaID).Msg("anilist: Updating media list entry")
|
||||
return ac.Client.UpdateMediaListEntry(ctx, mediaID, status, scoreRaw, progress, startedAt, completedAt, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) UpdateMediaListEntryProgress(ctx context.Context, mediaID *int, progress *int, status *MediaListStatus, interceptors ...clientv2.RequestInterceptor) (*UpdateMediaListEntryProgress, error) {
|
||||
if !ac.IsAuthenticated() {
|
||||
return nil, ErrNotAuthenticated
|
||||
}
|
||||
ac.logger.Debug().Int("mediaId", *mediaID).Msg("anilist: Updating media list entry progress")
|
||||
return ac.Client.UpdateMediaListEntryProgress(ctx, mediaID, progress, status, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) UpdateMediaListEntryRepeat(ctx context.Context, mediaID *int, repeat *int, interceptors ...clientv2.RequestInterceptor) (*UpdateMediaListEntryRepeat, error) {
|
||||
if !ac.IsAuthenticated() {
|
||||
return nil, ErrNotAuthenticated
|
||||
}
|
||||
ac.logger.Debug().Int("mediaId", *mediaID).Msg("anilist: Updating media list entry repeat")
|
||||
return ac.Client.UpdateMediaListEntryRepeat(ctx, mediaID, repeat, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) DeleteEntry(ctx context.Context, mediaListEntryID *int, interceptors ...clientv2.RequestInterceptor) (*DeleteEntry, error) {
|
||||
if !ac.IsAuthenticated() {
|
||||
return nil, ErrNotAuthenticated
|
||||
}
|
||||
ac.logger.Debug().Int("entryId", *mediaListEntryID).Msg("anilist: Deleting media list entry")
|
||||
return ac.Client.DeleteEntry(ctx, mediaListEntryID, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) AnimeCollection(ctx context.Context, userName *string, interceptors ...clientv2.RequestInterceptor) (*AnimeCollection, error) {
|
||||
if !ac.IsAuthenticated() {
|
||||
return nil, ErrNotAuthenticated
|
||||
}
|
||||
ac.logger.Debug().Msg("anilist: Fetching anime collection")
|
||||
return ac.Client.AnimeCollection(ctx, userName, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) AnimeCollectionWithRelations(ctx context.Context, userName *string, interceptors ...clientv2.RequestInterceptor) (*AnimeCollectionWithRelations, error) {
|
||||
if !ac.IsAuthenticated() {
|
||||
return nil, ErrNotAuthenticated
|
||||
}
|
||||
ac.logger.Debug().Msg("anilist: Fetching anime collection with relations")
|
||||
return ac.Client.AnimeCollectionWithRelations(ctx, userName, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) GetViewer(ctx context.Context, interceptors ...clientv2.RequestInterceptor) (*GetViewer, error) {
|
||||
if !ac.IsAuthenticated() {
|
||||
return nil, ErrNotAuthenticated
|
||||
}
|
||||
ac.logger.Debug().Msg("anilist: Fetching viewer")
|
||||
return ac.Client.GetViewer(ctx, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) MangaCollection(ctx context.Context, userName *string, interceptors ...clientv2.RequestInterceptor) (*MangaCollection, error) {
|
||||
if !ac.IsAuthenticated() {
|
||||
return nil, ErrNotAuthenticated
|
||||
}
|
||||
ac.logger.Debug().Msg("anilist: Fetching manga collection")
|
||||
return ac.Client.MangaCollection(ctx, userName, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) ViewerStats(ctx context.Context, interceptors ...clientv2.RequestInterceptor) (*ViewerStats, error) {
|
||||
if !ac.IsAuthenticated() {
|
||||
return nil, ErrNotAuthenticated
|
||||
}
|
||||
ac.logger.Debug().Msg("anilist: Fetching stats")
|
||||
return ac.Client.ViewerStats(ctx, interceptors...)
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// Not authenticated
|
||||
////////////////////////////////
|
||||
|
||||
func (ac *AnilistClientImpl) BaseAnimeByMalID(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*BaseAnimeByMalID, error) {
|
||||
return ac.Client.BaseAnimeByMalID(ctx, id, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) BaseAnimeByID(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*BaseAnimeByID, error) {
|
||||
ac.logger.Debug().Int("mediaId", *id).Msg("anilist: Fetching anime")
|
||||
return ac.Client.BaseAnimeByID(ctx, id, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) AnimeDetailsByID(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*AnimeDetailsByID, error) {
|
||||
ac.logger.Debug().Int("mediaId", *id).Msg("anilist: Fetching anime details")
|
||||
return ac.Client.AnimeDetailsByID(ctx, id, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) CompleteAnimeByID(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*CompleteAnimeByID, error) {
|
||||
ac.logger.Debug().Int("mediaId", *id).Msg("anilist: Fetching complete media")
|
||||
return ac.Client.CompleteAnimeByID(ctx, id, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) ListAnime(ctx context.Context, page *int, search *string, perPage *int, sort []*MediaSort, status []*MediaStatus, genres []*string, averageScoreGreater *int, season *MediaSeason, seasonYear *int, format *MediaFormat, isAdult *bool, interceptors ...clientv2.RequestInterceptor) (*ListAnime, error) {
|
||||
ac.logger.Debug().Msg("anilist: Fetching media list")
|
||||
return ac.Client.ListAnime(ctx, page, search, perPage, sort, status, genres, averageScoreGreater, season, seasonYear, format, isAdult, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) ListRecentAnime(ctx context.Context, page *int, perPage *int, airingAtGreater *int, airingAtLesser *int, notYetAired *bool, interceptors ...clientv2.RequestInterceptor) (*ListRecentAnime, error) {
|
||||
ac.logger.Debug().Msg("anilist: Fetching recent media list")
|
||||
return ac.Client.ListRecentAnime(ctx, page, perPage, airingAtGreater, airingAtLesser, notYetAired, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) SearchBaseManga(ctx context.Context, page *int, perPage *int, sort []*MediaSort, search *string, status []*MediaStatus, interceptors ...clientv2.RequestInterceptor) (*SearchBaseManga, error) {
|
||||
ac.logger.Debug().Msg("anilist: Searching manga")
|
||||
return ac.Client.SearchBaseManga(ctx, page, perPage, sort, search, status, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) BaseMangaByID(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*BaseMangaByID, error) {
|
||||
ac.logger.Debug().Int("mediaId", *id).Msg("anilist: Fetching manga")
|
||||
return ac.Client.BaseMangaByID(ctx, id, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) MangaDetailsByID(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*MangaDetailsByID, error) {
|
||||
ac.logger.Debug().Int("mediaId", *id).Msg("anilist: Fetching manga details")
|
||||
return ac.Client.MangaDetailsByID(ctx, id, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) ListManga(ctx context.Context, page *int, search *string, perPage *int, sort []*MediaSort, status []*MediaStatus, genres []*string, averageScoreGreater *int, startDateGreater *string, startDateLesser *string, format *MediaFormat, countryOfOrigin *string, isAdult *bool, interceptors ...clientv2.RequestInterceptor) (*ListManga, error) {
|
||||
ac.logger.Debug().Msg("anilist: Fetching manga list")
|
||||
return ac.Client.ListManga(ctx, page, search, perPage, sort, status, genres, averageScoreGreater, startDateGreater, startDateLesser, format, countryOfOrigin, isAdult, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) StudioDetails(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*StudioDetails, error) {
|
||||
ac.logger.Debug().Int("studioId", *id).Msg("anilist: Fetching studio details")
|
||||
return ac.Client.StudioDetails(ctx, id, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) SearchBaseAnimeByIds(ctx context.Context, ids []*int, page *int, perPage *int, status []*MediaStatus, inCollection *bool, sort []*MediaSort, season *MediaSeason, year *int, genre *string, format *MediaFormat, interceptors ...clientv2.RequestInterceptor) (*SearchBaseAnimeByIds, error) {
|
||||
ac.logger.Debug().Msg("anilist: Searching anime by ids")
|
||||
return ac.Client.SearchBaseAnimeByIds(ctx, ids, page, perPage, status, inCollection, sort, season, year, genre, format, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) AnimeAiringSchedule(ctx context.Context, ids []*int, season *MediaSeason, seasonYear *int, previousSeason *MediaSeason, previousSeasonYear *int, nextSeason *MediaSeason, nextSeasonYear *int, interceptors ...clientv2.RequestInterceptor) (*AnimeAiringSchedule, error) {
|
||||
ac.logger.Debug().Msg("anilist: Fetching schedule")
|
||||
return ac.Client.AnimeAiringSchedule(ctx, ids, season, seasonYear, previousSeason, previousSeasonYear, nextSeason, nextSeasonYear, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *AnilistClientImpl) AnimeAiringScheduleRaw(ctx context.Context, ids []*int, interceptors ...clientv2.RequestInterceptor) (*AnimeAiringScheduleRaw, error) {
|
||||
ac.logger.Debug().Msg("anilist: Fetching schedule")
|
||||
return ac.Client.AnimeAiringScheduleRaw(ctx, ids, interceptors...)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
var sentRateLimitWarningTime = time.Now().Add(-10 * time.Second)
|
||||
|
||||
// customDoFunc is a custom request interceptor function that handles rate limiting and retries.
|
||||
func (ac *AnilistClientImpl) customDoFunc(ctx context.Context, req *http.Request, gqlInfo *clientv2.GQLRequestInfo, res interface{}) (err error) {
|
||||
var rlRemainingStr string
|
||||
|
||||
reqTime := time.Now()
|
||||
defer func() {
|
||||
timeSince := time.Since(reqTime)
|
||||
formattedDur := timeSince.Truncate(time.Millisecond).String()
|
||||
if err != nil {
|
||||
ac.logger.Error().Str("duration", formattedDur).Str("rlr", rlRemainingStr).Err(err).Msg("anilist: Failed Request")
|
||||
} else {
|
||||
if timeSince > 900*time.Millisecond {
|
||||
ac.logger.Warn().Str("rtt", formattedDur).Str("rlr", rlRemainingStr).Msg("anilist: Successful Request (slow)")
|
||||
} else {
|
||||
ac.logger.Info().Str("rtt", formattedDur).Str("rlr", rlRemainingStr).Msg("anilist: Successful Request")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
client := http.DefaultClient
|
||||
var resp *http.Response
|
||||
|
||||
retryCount := 2
|
||||
|
||||
for i := 0; i < retryCount; i++ {
|
||||
|
||||
// Reset response body for retry
|
||||
if resp != nil && resp.Body != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
// Recreate the request body if it was read in a previous attempt
|
||||
if req.GetBody != nil {
|
||||
newBody, err := req.GetBody()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get request body: %w", err)
|
||||
}
|
||||
req.Body = newBody
|
||||
}
|
||||
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
|
||||
rlRemainingStr = resp.Header.Get("X-Ratelimit-Remaining")
|
||||
rlRetryAfterStr := resp.Header.Get("Retry-After")
|
||||
//println("Remaining:", rlRemainingStr, " | RetryAfter:", rlRetryAfterStr)
|
||||
|
||||
// If we have a rate limit, sleep for the time
|
||||
rlRetryAfter, err := strconv.Atoi(rlRetryAfterStr)
|
||||
if err == nil {
|
||||
ac.logger.Warn().Msgf("anilist: Rate limited, retrying in %d seconds", rlRetryAfter+1)
|
||||
if time.Since(sentRateLimitWarningTime) > 10*time.Second {
|
||||
events.GlobalWSEventManager.SendEvent(events.WarningToast, "anilist: Rate limited, retrying in "+strconv.Itoa(rlRetryAfter+1)+" seconds")
|
||||
sentRateLimitWarningTime = time.Now()
|
||||
}
|
||||
select {
|
||||
case <-time.After(time.Duration(rlRetryAfter+1) * time.Second):
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if rlRemainingStr == "" {
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.Header.Get("Content-Encoding") == "gzip" {
|
||||
resp.Body, err = gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("gzip decode failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var body []byte
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
err = parseResponse(body, resp.StatusCode, res)
|
||||
return
|
||||
}
|
||||
|
||||
func parseResponse(body []byte, httpCode int, result interface{}) error {
|
||||
errResponse := &clientv2.ErrorResponse{}
|
||||
isKOCode := httpCode < 200 || 299 < httpCode
|
||||
if isKOCode {
|
||||
errResponse.NetworkError = &clientv2.HTTPError{
|
||||
Code: httpCode,
|
||||
Message: fmt.Sprintf("Response body %s", string(body)),
|
||||
}
|
||||
}
|
||||
|
||||
// some servers return a graphql error with a non OK http code, try anyway to parse the body
|
||||
if err := unmarshal(body, result); err != nil {
|
||||
var gqlErr *clientv2.GqlErrorList
|
||||
if errors.As(err, &gqlErr) {
|
||||
errResponse.GqlErrors = &gqlErr.Errors
|
||||
} else if !isKOCode {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if errResponse.HasErrors() {
|
||||
return errResponse
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// response is a GraphQL layer response from a handler.
|
||||
type response struct {
|
||||
Data json.RawMessage `json:"data"`
|
||||
Errors json.RawMessage `json:"errors"`
|
||||
}
|
||||
|
||||
func unmarshal(data []byte, res interface{}) error {
|
||||
ParseDataWhenErrors := false
|
||||
resp := response{}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return fmt.Errorf("failed to decode data %s: %w", string(data), err)
|
||||
}
|
||||
|
||||
var err error
|
||||
if resp.Errors != nil && len(resp.Errors) > 0 {
|
||||
// try to parse standard graphql error
|
||||
err = &clientv2.GqlErrorList{}
|
||||
if e := json.Unmarshal(data, err); e != nil {
|
||||
return fmt.Errorf("faild to parse graphql errors. Response content %s - %w", string(data), e)
|
||||
}
|
||||
|
||||
// if ParseDataWhenErrors is true, try to parse data as well
|
||||
if !ParseDataWhenErrors {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if errData := graphqljson.UnmarshalData(resp.Data, res); errData != nil {
|
||||
// if ParseDataWhenErrors is true, and we failed to unmarshal data, return the actual error
|
||||
if ParseDataWhenErrors {
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to decode data into response %s: %w", string(data), errData)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
9542
seanime-2.9.10/internal/api/anilist/client_gen.go
Normal file
9542
seanime-2.9.10/internal/api/anilist/client_gen.go
Normal file
File diff suppressed because it is too large
Load Diff
569
seanime-2.9.10/internal/api/anilist/client_mock.go
Normal file
569
seanime-2.9.10/internal/api/anilist/client_mock.go
Normal file
@@ -0,0 +1,569 @@
|
||||
package anilist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"seanime/internal/test_utils"
|
||||
"seanime/internal/util"
|
||||
|
||||
"github.com/Yamashou/gqlgenc/clientv2"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// This file contains helper functions for testing the anilist package
|
||||
|
||||
func TestGetMockAnilistClient() AnilistClient {
|
||||
return NewMockAnilistClient()
|
||||
}
|
||||
|
||||
// MockAnilistClientImpl is a mock implementation of the AnilistClient, used for tests.
|
||||
// It uses the real implementation of the AnilistClient to make requests then populates a cache with the results.
|
||||
// This is to avoid making repeated requests to the AniList API during tests but still have realistic data.
|
||||
type MockAnilistClientImpl struct {
|
||||
realAnilistClient AnilistClient
|
||||
logger *zerolog.Logger
|
||||
}
|
||||
|
||||
func NewMockAnilistClient() *MockAnilistClientImpl {
|
||||
return &MockAnilistClientImpl{
|
||||
realAnilistClient: NewAnilistClient(test_utils.ConfigData.Provider.AnilistJwt),
|
||||
logger: util.NewLogger(),
|
||||
}
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) IsAuthenticated() bool {
|
||||
return ac.realAnilistClient.IsAuthenticated()
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) BaseAnimeByMalID(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*BaseAnimeByMalID, error) {
|
||||
file, err := os.Open(test_utils.GetTestDataPath("BaseAnimeByMalID"))
|
||||
defer file.Close()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
ac.logger.Warn().Msgf("MockAnilistClientImpl: CACHE MISS [BaseAnimeByMalID]: %d", *id)
|
||||
ret, err := ac.realAnilistClient.BaseAnimeByMalID(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := json.Marshal([]*BaseAnimeByMalID{ret})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = os.WriteFile(test_utils.GetTestDataPath("BaseAnimeByMalID"), data, 0644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
}
|
||||
|
||||
var media []*BaseAnimeByMalID
|
||||
err = json.NewDecoder(file).Decode(&media)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
var ret *BaseAnimeByMalID
|
||||
for _, m := range media {
|
||||
if m.GetMedia().ID == *id {
|
||||
ret = m
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ret == nil {
|
||||
ac.logger.Warn().Msgf("MockAnilistClientImpl: CACHE MISS [BaseAnimeByMalID]: %d", *id)
|
||||
ret, err := ac.realAnilistClient.BaseAnimeByMalID(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
media = append(media, ret)
|
||||
data, err := json.Marshal(media)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = os.WriteFile(test_utils.GetTestDataPath("BaseAnimeByMalID"), data, 0644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
ac.logger.Trace().Msgf("MockAnilistClientImpl: CACHE HIT [BaseAnimeByMalID]: %d", *id)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) BaseAnimeByID(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*BaseAnimeByID, error) {
|
||||
file, err := os.Open(test_utils.GetTestDataPath("BaseAnimeByID"))
|
||||
defer file.Close()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
ac.logger.Warn().Msgf("MockAnilistClientImpl: CACHE MISS [BaseAnimeByID]: %d", *id)
|
||||
baseAnime, err := ac.realAnilistClient.BaseAnimeByID(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := json.Marshal([]*BaseAnimeByID{baseAnime})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = os.WriteFile(test_utils.GetTestDataPath("BaseAnimeByID"), data, 0644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return baseAnime, nil
|
||||
}
|
||||
}
|
||||
|
||||
var media []*BaseAnimeByID
|
||||
err = json.NewDecoder(file).Decode(&media)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
var baseAnime *BaseAnimeByID
|
||||
for _, m := range media {
|
||||
if m.GetMedia().ID == *id {
|
||||
baseAnime = m
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if baseAnime == nil {
|
||||
ac.logger.Warn().Msgf("MockAnilistClientImpl: CACHE MISS [BaseAnimeByID]: %d", *id)
|
||||
baseAnime, err := ac.realAnilistClient.BaseAnimeByID(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
media = append(media, baseAnime)
|
||||
data, err := json.Marshal(media)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = os.WriteFile(test_utils.GetTestDataPath("BaseAnimeByID"), data, 0644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return baseAnime, nil
|
||||
}
|
||||
|
||||
ac.logger.Trace().Msgf("MockAnilistClientImpl: CACHE HIT [BaseAnimeByID]: %d", *id)
|
||||
return baseAnime, nil
|
||||
}
|
||||
|
||||
// AnimeCollection
|
||||
// - Set userName to nil to use the boilerplate AnimeCollection
|
||||
// - Set userName to a specific username to fetch and cache
|
||||
func (ac *MockAnilistClientImpl) AnimeCollection(ctx context.Context, userName *string, interceptors ...clientv2.RequestInterceptor) (*AnimeCollection, error) {
|
||||
|
||||
if userName == nil {
|
||||
file, err := os.Open(test_utils.GetDataPath("BoilerplateAnimeCollection"))
|
||||
defer file.Close()
|
||||
|
||||
var ret *AnimeCollection
|
||||
err = json.NewDecoder(file).Decode(&ret)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ac.logger.Trace().Msgf("MockAnilistClientImpl: Using [BoilerplateAnimeCollection]")
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
file, err := os.Open(test_utils.GetTestDataPath("AnimeCollection"))
|
||||
defer file.Close()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
ac.logger.Warn().Msgf("MockAnilistClientImpl: CACHE MISS [AnimeCollection]: %s", *userName)
|
||||
ret, err := ac.realAnilistClient.AnimeCollection(context.Background(), userName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := json.Marshal(ret)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = os.WriteFile(test_utils.GetTestDataPath("AnimeCollection"), data, 0644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
}
|
||||
|
||||
var ret *AnimeCollection
|
||||
err = json.NewDecoder(file).Decode(&ret)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if ret == nil {
|
||||
ac.logger.Warn().Msgf("MockAnilistClientImpl: CACHE MISS [AnimeCollection]: %s", *userName)
|
||||
ret, err := ac.realAnilistClient.AnimeCollection(context.Background(), userName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := json.Marshal(ret)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = os.WriteFile(test_utils.GetTestDataPath("AnimeCollection"), data, 0644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
ac.logger.Trace().Msgf("MockAnilistClientImpl: CACHE HIT [AnimeCollection]: %s", *userName)
|
||||
return ret, nil
|
||||
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) AnimeCollectionWithRelations(ctx context.Context, userName *string, interceptors ...clientv2.RequestInterceptor) (*AnimeCollectionWithRelations, error) {
|
||||
|
||||
if userName == nil {
|
||||
file, err := os.Open(test_utils.GetDataPath("BoilerplateAnimeCollectionWithRelations"))
|
||||
defer file.Close()
|
||||
|
||||
var ret *AnimeCollectionWithRelations
|
||||
err = json.NewDecoder(file).Decode(&ret)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ac.logger.Trace().Msgf("MockAnilistClientImpl: Using [BoilerplateAnimeCollectionWithRelations]")
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
file, err := os.Open(test_utils.GetTestDataPath("AnimeCollectionWithRelations"))
|
||||
defer file.Close()
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
ac.logger.Warn().Msgf("MockAnilistClientImpl: CACHE MISS [AnimeCollectionWithRelations]: %s", *userName)
|
||||
ret, err := ac.realAnilistClient.AnimeCollectionWithRelations(context.Background(), userName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := json.Marshal(ret)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = os.WriteFile(test_utils.GetTestDataPath("AnimeCollectionWithRelations"), data, 0644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
}
|
||||
|
||||
var ret *AnimeCollectionWithRelations
|
||||
err = json.NewDecoder(file).Decode(&ret)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if ret == nil {
|
||||
ac.logger.Warn().Msgf("MockAnilistClientImpl: CACHE MISS [AnimeCollectionWithRelations]: %s", *userName)
|
||||
ret, err := ac.realAnilistClient.AnimeCollectionWithRelations(context.Background(), userName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := json.Marshal(ret)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = os.WriteFile(test_utils.GetTestDataPath("AnimeCollectionWithRelations"), data, 0644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
ac.logger.Trace().Msgf("MockAnilistClientImpl: CACHE HIT [AnimeCollectionWithRelations]: %s", *userName)
|
||||
return ret, nil
|
||||
|
||||
}
|
||||
|
||||
type TestModifyAnimeCollectionEntryInput struct {
|
||||
Status *MediaListStatus
|
||||
Progress *int
|
||||
Score *float64
|
||||
AiredEpisodes *int
|
||||
NextAiringEpisode *BaseAnime_NextAiringEpisode
|
||||
}
|
||||
|
||||
// TestModifyAnimeCollectionEntry will modify an entry in the fetched anime collection.
|
||||
// This is used to fine-tune the anime collection for testing purposes.
|
||||
//
|
||||
// Example: Setting a specific progress in case the origin anime collection has no progress
|
||||
func TestModifyAnimeCollectionEntry(ac *AnimeCollection, mId int, input TestModifyAnimeCollectionEntryInput) *AnimeCollection {
|
||||
if ac == nil {
|
||||
panic("AnimeCollection is nil")
|
||||
}
|
||||
|
||||
lists := ac.GetMediaListCollection().GetLists()
|
||||
|
||||
removedFromList := false
|
||||
var rEntry *AnimeCollection_MediaListCollection_Lists_Entries
|
||||
|
||||
// Move the entry to the correct list
|
||||
if input.Status != nil {
|
||||
for _, list := range lists {
|
||||
if list.Status == nil || list.Entries == nil {
|
||||
continue
|
||||
}
|
||||
entries := list.GetEntries()
|
||||
for idx, entry := range entries {
|
||||
if entry.GetMedia().ID == mId {
|
||||
// Remove from current list if status differs
|
||||
if *list.Status != *input.Status {
|
||||
removedFromList = true
|
||||
rEntry = entry
|
||||
// Ensure we're not going out of bounds
|
||||
if idx >= 0 && idx < len(entries) {
|
||||
// Safely remove the entry by re-slicing
|
||||
list.Entries = append(entries[:idx], entries[idx+1:]...)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the entry to the correct list if it was removed
|
||||
if removedFromList && rEntry != nil {
|
||||
for _, list := range lists {
|
||||
if list.Status == nil {
|
||||
continue
|
||||
}
|
||||
if *list.Status == *input.Status {
|
||||
if list.Entries == nil {
|
||||
list.Entries = make([]*AnimeCollection_MediaListCollection_Lists_Entries, 0)
|
||||
}
|
||||
// Add the removed entry to the new list
|
||||
list.Entries = append(list.Entries, rEntry)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the entry details
|
||||
out:
|
||||
for _, list := range lists {
|
||||
entries := list.GetEntries()
|
||||
for _, entry := range entries {
|
||||
if entry.GetMedia().ID == mId {
|
||||
if input.Status != nil {
|
||||
entry.Status = input.Status
|
||||
}
|
||||
if input.Progress != nil {
|
||||
entry.Progress = input.Progress
|
||||
}
|
||||
if input.Score != nil {
|
||||
entry.Score = input.Score
|
||||
}
|
||||
if input.AiredEpisodes != nil {
|
||||
entry.Media.Episodes = input.AiredEpisodes
|
||||
}
|
||||
if input.NextAiringEpisode != nil {
|
||||
entry.Media.NextAiringEpisode = input.NextAiringEpisode
|
||||
}
|
||||
break out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ac
|
||||
}
|
||||
|
||||
func TestAddAnimeCollectionEntry(ac *AnimeCollection, mId int, input TestModifyAnimeCollectionEntryInput, realClient AnilistClient) *AnimeCollection {
|
||||
if ac == nil {
|
||||
panic("AnimeCollection is nil")
|
||||
}
|
||||
|
||||
// Fetch the anime details
|
||||
baseAnime, err := realClient.BaseAnimeByID(context.Background(), &mId)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
anime := baseAnime.GetMedia()
|
||||
|
||||
if input.NextAiringEpisode != nil {
|
||||
anime.NextAiringEpisode = input.NextAiringEpisode
|
||||
}
|
||||
|
||||
if input.AiredEpisodes != nil {
|
||||
anime.Episodes = input.AiredEpisodes
|
||||
}
|
||||
|
||||
lists := ac.GetMediaListCollection().GetLists()
|
||||
|
||||
// Add the entry to the correct list
|
||||
if input.Status != nil {
|
||||
for _, list := range lists {
|
||||
if list.Status == nil {
|
||||
continue
|
||||
}
|
||||
if *list.Status == *input.Status {
|
||||
if list.Entries == nil {
|
||||
list.Entries = make([]*AnimeCollection_MediaListCollection_Lists_Entries, 0)
|
||||
}
|
||||
list.Entries = append(list.Entries, &AnimeCollection_MediaListCollection_Lists_Entries{
|
||||
Media: baseAnime.GetMedia(),
|
||||
Status: input.Status,
|
||||
Progress: input.Progress,
|
||||
Score: input.Score,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ac
|
||||
}
|
||||
|
||||
func TestAddAnimeCollectionWithRelationsEntry(ac *AnimeCollectionWithRelations, mId int, input TestModifyAnimeCollectionEntryInput, realClient AnilistClient) *AnimeCollectionWithRelations {
|
||||
if ac == nil {
|
||||
panic("AnimeCollection is nil")
|
||||
}
|
||||
|
||||
// Fetch the anime details
|
||||
baseAnime, err := realClient.CompleteAnimeByID(context.Background(), &mId)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
anime := baseAnime.GetMedia()
|
||||
|
||||
//if input.NextAiringEpisode != nil {
|
||||
// anime.NextAiringEpisode = input.NextAiringEpisode
|
||||
//}
|
||||
|
||||
if input.AiredEpisodes != nil {
|
||||
anime.Episodes = input.AiredEpisodes
|
||||
}
|
||||
|
||||
lists := ac.GetMediaListCollection().GetLists()
|
||||
|
||||
// Add the entry to the correct list
|
||||
if input.Status != nil {
|
||||
for _, list := range lists {
|
||||
if list.Status == nil {
|
||||
continue
|
||||
}
|
||||
if *list.Status == *input.Status {
|
||||
if list.Entries == nil {
|
||||
list.Entries = make([]*AnimeCollectionWithRelations_MediaListCollection_Lists_Entries, 0)
|
||||
}
|
||||
list.Entries = append(list.Entries, &AnimeCollectionWithRelations_MediaListCollection_Lists_Entries{
|
||||
Media: baseAnime.GetMedia(),
|
||||
Status: input.Status,
|
||||
Progress: input.Progress,
|
||||
Score: input.Score,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ac
|
||||
}
|
||||
|
||||
//
|
||||
// WILL NOT IMPLEMENT
|
||||
//
|
||||
|
||||
func (ac *MockAnilistClientImpl) UpdateMediaListEntry(ctx context.Context, mediaID *int, status *MediaListStatus, scoreRaw *int, progress *int, startedAt *FuzzyDateInput, completedAt *FuzzyDateInput, interceptors ...clientv2.RequestInterceptor) (*UpdateMediaListEntry, error) {
|
||||
ac.logger.Debug().Int("mediaId", *mediaID).Msg("anilist: Updating media list entry")
|
||||
return &UpdateMediaListEntry{}, nil
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) UpdateMediaListEntryProgress(ctx context.Context, mediaID *int, progress *int, status *MediaListStatus, interceptors ...clientv2.RequestInterceptor) (*UpdateMediaListEntryProgress, error) {
|
||||
ac.logger.Debug().Int("mediaId", *mediaID).Msg("anilist: Updating media list entry progress")
|
||||
return &UpdateMediaListEntryProgress{}, nil
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) UpdateMediaListEntryRepeat(ctx context.Context, mediaID *int, repeat *int, interceptors ...clientv2.RequestInterceptor) (*UpdateMediaListEntryRepeat, error) {
|
||||
ac.logger.Debug().Int("mediaId", *mediaID).Msg("anilist: Updating media list entry repeat")
|
||||
return &UpdateMediaListEntryRepeat{}, nil
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) DeleteEntry(ctx context.Context, mediaListEntryID *int, interceptors ...clientv2.RequestInterceptor) (*DeleteEntry, error) {
|
||||
ac.logger.Debug().Int("entryId", *mediaListEntryID).Msg("anilist: Deleting media list entry")
|
||||
return &DeleteEntry{}, nil
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) AnimeDetailsByID(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*AnimeDetailsByID, error) {
|
||||
ac.logger.Debug().Int("mediaId", *id).Msg("anilist: Fetching anime details")
|
||||
return ac.realAnilistClient.AnimeDetailsByID(ctx, id, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) CompleteAnimeByID(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*CompleteAnimeByID, error) {
|
||||
ac.logger.Debug().Int("mediaId", *id).Msg("anilist: Fetching complete media")
|
||||
return ac.realAnilistClient.CompleteAnimeByID(ctx, id, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) ListAnime(ctx context.Context, page *int, search *string, perPage *int, sort []*MediaSort, status []*MediaStatus, genres []*string, averageScoreGreater *int, season *MediaSeason, seasonYear *int, format *MediaFormat, isAdult *bool, interceptors ...clientv2.RequestInterceptor) (*ListAnime, error) {
|
||||
ac.logger.Debug().Msg("anilist: Fetching media list")
|
||||
return ac.realAnilistClient.ListAnime(ctx, page, search, perPage, sort, status, genres, averageScoreGreater, season, seasonYear, format, isAdult, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) ListRecentAnime(ctx context.Context, page *int, perPage *int, airingAtGreater *int, airingAtLesser *int, notYetAired *bool, interceptors ...clientv2.RequestInterceptor) (*ListRecentAnime, error) {
|
||||
ac.logger.Debug().Msg("anilist: Fetching recent media list")
|
||||
return ac.realAnilistClient.ListRecentAnime(ctx, page, perPage, airingAtGreater, airingAtLesser, notYetAired, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) GetViewer(ctx context.Context, interceptors ...clientv2.RequestInterceptor) (*GetViewer, error) {
|
||||
ac.logger.Debug().Msg("anilist: Fetching viewer")
|
||||
return ac.realAnilistClient.GetViewer(ctx, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) MangaCollection(ctx context.Context, userName *string, interceptors ...clientv2.RequestInterceptor) (*MangaCollection, error) {
|
||||
ac.logger.Debug().Msg("anilist: Fetching manga collection")
|
||||
return ac.realAnilistClient.MangaCollection(ctx, userName, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) SearchBaseManga(ctx context.Context, page *int, perPage *int, sort []*MediaSort, search *string, status []*MediaStatus, interceptors ...clientv2.RequestInterceptor) (*SearchBaseManga, error) {
|
||||
ac.logger.Debug().Msg("anilist: Searching manga")
|
||||
return ac.realAnilistClient.SearchBaseManga(ctx, page, perPage, sort, search, status, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) BaseMangaByID(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*BaseMangaByID, error) {
|
||||
ac.logger.Debug().Int("mediaId", *id).Msg("anilist: Fetching manga")
|
||||
return ac.realAnilistClient.BaseMangaByID(ctx, id, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) MangaDetailsByID(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*MangaDetailsByID, error) {
|
||||
ac.logger.Debug().Int("mediaId", *id).Msg("anilist: Fetching manga details")
|
||||
return ac.realAnilistClient.MangaDetailsByID(ctx, id, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) ListManga(ctx context.Context, page *int, search *string, perPage *int, sort []*MediaSort, status []*MediaStatus, genres []*string, averageScoreGreater *int, startDateGreater *string, startDateLesser *string, format *MediaFormat, countryOfOrigin *string, isAdult *bool, interceptors ...clientv2.RequestInterceptor) (*ListManga, error) {
|
||||
ac.logger.Debug().Msg("anilist: Fetching manga list")
|
||||
return ac.realAnilistClient.ListManga(ctx, page, search, perPage, sort, status, genres, averageScoreGreater, startDateGreater, startDateLesser, format, countryOfOrigin, isAdult, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) StudioDetails(ctx context.Context, id *int, interceptors ...clientv2.RequestInterceptor) (*StudioDetails, error) {
|
||||
ac.logger.Debug().Int("studioId", *id).Msg("anilist: Fetching studio details")
|
||||
return ac.realAnilistClient.StudioDetails(ctx, id, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) ViewerStats(ctx context.Context, interceptors ...clientv2.RequestInterceptor) (*ViewerStats, error) {
|
||||
ac.logger.Debug().Msg("anilist: Fetching stats")
|
||||
return ac.realAnilistClient.ViewerStats(ctx, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) SearchBaseAnimeByIds(ctx context.Context, ids []*int, page *int, perPage *int, status []*MediaStatus, inCollection *bool, sort []*MediaSort, season *MediaSeason, year *int, genre *string, format *MediaFormat, interceptors ...clientv2.RequestInterceptor) (*SearchBaseAnimeByIds, error) {
|
||||
ac.logger.Debug().Msg("anilist: Searching anime by ids")
|
||||
return ac.realAnilistClient.SearchBaseAnimeByIds(ctx, ids, page, perPage, status, inCollection, sort, season, year, genre, format, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) AnimeAiringSchedule(ctx context.Context, ids []*int, season *MediaSeason, seasonYear *int, previousSeason *MediaSeason, previousSeasonYear *int, nextSeason *MediaSeason, nextSeasonYear *int, interceptors ...clientv2.RequestInterceptor) (*AnimeAiringSchedule, error) {
|
||||
ac.logger.Debug().Msg("anilist: Fetching schedule")
|
||||
return ac.realAnilistClient.AnimeAiringSchedule(ctx, ids, season, seasonYear, previousSeason, previousSeasonYear, nextSeason, nextSeasonYear, interceptors...)
|
||||
}
|
||||
|
||||
func (ac *MockAnilistClientImpl) AnimeAiringScheduleRaw(ctx context.Context, ids []*int, interceptors ...clientv2.RequestInterceptor) (*AnimeAiringScheduleRaw, error) {
|
||||
ac.logger.Debug().Msg("anilist: Fetching schedule")
|
||||
return ac.realAnilistClient.AnimeAiringScheduleRaw(ctx, ids, interceptors...)
|
||||
}
|
||||
73
seanime-2.9.10/internal/api/anilist/client_mock_test.go
Normal file
73
seanime-2.9.10/internal/api/anilist/client_mock_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package anilist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"seanime/internal/test_utils"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// USE CASE: Generate a boilerplate Anilist AnimeCollection for testing purposes and save it to 'test/data/BoilerplateAnimeCollection'.
|
||||
// The generated AnimeCollection will have all entries in the 'Planning' status.
|
||||
// The generated AnimeCollection will be used to test various Anilist API methods.
|
||||
// You can use TestModifyAnimeCollectionEntry to modify the generated AnimeCollection before using it in a test.
|
||||
// - DO NOT RUN IF YOU DON'T PLAN TO GENERATE A NEW 'test/data/BoilerplateAnimeCollection'
|
||||
func TestGenerateBoilerplateAnimeCollection(t *testing.T) {
|
||||
t.Skip("This test is not meant to be run")
|
||||
test_utils.InitTestProvider(t, test_utils.Anilist())
|
||||
|
||||
anilistClient := TestGetMockAnilistClient()
|
||||
|
||||
ac, err := anilistClient.AnimeCollection(context.Background(), &test_utils.ConfigData.Provider.AnilistUsername)
|
||||
|
||||
if assert.NoError(t, err) {
|
||||
|
||||
lists := ac.GetMediaListCollection().GetLists()
|
||||
|
||||
entriesToAddToPlanning := make([]*AnimeListEntry, 0)
|
||||
|
||||
if assert.NoError(t, err) {
|
||||
|
||||
for _, list := range lists {
|
||||
if list.Status != nil {
|
||||
if list.GetStatus().String() != string(MediaListStatusPlanning) {
|
||||
entries := list.GetEntries()
|
||||
for _, entry := range entries {
|
||||
entry.Progress = lo.ToPtr(0)
|
||||
entry.Score = lo.ToPtr(0.0)
|
||||
entry.Status = lo.ToPtr(MediaListStatusPlanning)
|
||||
entriesToAddToPlanning = append(entriesToAddToPlanning, entry)
|
||||
}
|
||||
list.Entries = make([]*AnimeListEntry, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newLists := make([]*AnimeCollection_MediaListCollection_Lists, 0)
|
||||
for _, list := range lists {
|
||||
if list.Status == nil {
|
||||
continue
|
||||
}
|
||||
if *list.GetStatus() == MediaListStatusPlanning {
|
||||
list.Entries = append(list.Entries, entriesToAddToPlanning...)
|
||||
newLists = append(newLists, list)
|
||||
} else {
|
||||
newLists = append(newLists, list)
|
||||
}
|
||||
}
|
||||
|
||||
ac.MediaListCollection.Lists = newLists
|
||||
|
||||
data, err := json.Marshal(ac)
|
||||
if assert.NoError(t, err) {
|
||||
err = os.WriteFile(test_utils.GetDataPath("BoilerplateAnimeCollection"), data, 0644)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
237
seanime-2.9.10/internal/api/anilist/client_test.go
Normal file
237
seanime-2.9.10/internal/api/anilist/client_test.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package anilist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"seanime/internal/test_utils"
|
||||
"seanime/internal/util"
|
||||
"testing"
|
||||
)
|
||||
|
||||
//func TestHiddenFromStatus(t *testing.T) {
|
||||
// test_utils.InitTestProvider(t, test_utils.Anilist())
|
||||
//
|
||||
// token := test_utils.ConfigData.Provider.AnilistJwt
|
||||
// logger := util.NewLogger()
|
||||
// //anilistClient := NewAnilistClient(test_utils.ConfigData.Provider.AnilistJwt)
|
||||
//
|
||||
// variables := map[string]interface{}{}
|
||||
//
|
||||
// variables["userName"] = test_utils.ConfigData.Provider.AnilistUsername
|
||||
// variables["type"] = "ANIME"
|
||||
//
|
||||
// requestBody, err := json.Marshal(map[string]interface{}{
|
||||
// "query": testQuery,
|
||||
// "variables": variables,
|
||||
// })
|
||||
// require.NoError(t, err)
|
||||
//
|
||||
// data, err := customQuery(requestBody, logger, token)
|
||||
// require.NoError(t, err)
|
||||
//
|
||||
// var mediaLists []*MediaList
|
||||
//
|
||||
// type retData struct {
|
||||
// Page Page
|
||||
// PageInfo PageInfo
|
||||
// }
|
||||
//
|
||||
// var ret retData
|
||||
// m, err := json.Marshal(data)
|
||||
// require.NoError(t, err)
|
||||
// if err := json.Unmarshal(m, &ret); err != nil {
|
||||
// t.Fatalf("Failed to unmarshal data: %v", err)
|
||||
// }
|
||||
//
|
||||
// mediaLists = append(mediaLists, ret.Page.MediaList...)
|
||||
//
|
||||
// util.Spew(ret.Page.PageInfo)
|
||||
//
|
||||
// var currentPage = 1
|
||||
// var hasNextPage = false
|
||||
// if ret.Page.PageInfo != nil && ret.Page.PageInfo.HasNextPage != nil {
|
||||
// hasNextPage = *ret.Page.PageInfo.HasNextPage
|
||||
// }
|
||||
// for hasNextPage {
|
||||
// currentPage++
|
||||
// variables["page"] = currentPage
|
||||
// requestBody, err = json.Marshal(map[string]interface{}{
|
||||
// "query": testQuery,
|
||||
// "variables": variables,
|
||||
// })
|
||||
// require.NoError(t, err)
|
||||
// data, err = customQuery(requestBody, logger, token)
|
||||
// require.NoError(t, err)
|
||||
// m, err = json.Marshal(data)
|
||||
// require.NoError(t, err)
|
||||
// if err := json.Unmarshal(m, &ret); err != nil {
|
||||
// t.Fatalf("Failed to unmarshal data: %v", err)
|
||||
// }
|
||||
// util.Spew(ret.Page.PageInfo)
|
||||
// if ret.Page.PageInfo != nil && ret.Page.PageInfo.HasNextPage != nil {
|
||||
// hasNextPage = *ret.Page.PageInfo.HasNextPage
|
||||
// }
|
||||
// mediaLists = append(mediaLists, ret.Page.MediaList...)
|
||||
// }
|
||||
//
|
||||
// //res, err := anilistClient.AnimeCollection(context.Background(), &test_utils.ConfigData.Provider.AnilistUsername)
|
||||
// //assert.NoError(t, err)
|
||||
//
|
||||
// for _, mediaList := range mediaLists {
|
||||
// util.Spew(mediaList.Media.ID)
|
||||
// if mediaList.Media.ID == 151514 {
|
||||
// util.Spew(mediaList)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//}
|
||||
//
|
||||
//const testQuery = `query ($page: Int, $userName: String, $type: MediaType) {
|
||||
// Page (page: $page, perPage: 100) {
|
||||
// pageInfo {
|
||||
// hasNextPage
|
||||
// total
|
||||
// perPage
|
||||
// currentPage
|
||||
// lastPage
|
||||
// }
|
||||
// mediaList (type: $type, userName: $userName) {
|
||||
// status
|
||||
// startedAt {
|
||||
// year
|
||||
// month
|
||||
// day
|
||||
// }
|
||||
// completedAt {
|
||||
// year
|
||||
// month
|
||||
// day
|
||||
// }
|
||||
// repeat
|
||||
// score(format: POINT_100)
|
||||
// progress
|
||||
// progressVolumes
|
||||
// notes
|
||||
// media {
|
||||
// siteUrl
|
||||
// id
|
||||
// idMal
|
||||
// episodes
|
||||
// chapters
|
||||
// volumes
|
||||
// status
|
||||
// averageScore
|
||||
// coverImage{
|
||||
// large
|
||||
// extraLarge
|
||||
// }
|
||||
// bannerImage
|
||||
// title {
|
||||
// userPreferred
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }`
|
||||
|
||||
func TestGetAnimeById(t *testing.T) {
|
||||
test_utils.InitTestProvider(t, test_utils.Anilist())
|
||||
|
||||
anilistClient := TestGetMockAnilistClient()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mediaId int
|
||||
}{
|
||||
{
|
||||
name: "Cowboy Bebop",
|
||||
mediaId: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
res, err := anilistClient.BaseAnimeByID(context.Background(), &tt.mediaId)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAnime(t *testing.T) {
|
||||
test_utils.InitTestProvider(t, test_utils.Anilist())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
Page *int
|
||||
Search *string
|
||||
PerPage *int
|
||||
Sort []*MediaSort
|
||||
Status []*MediaStatus
|
||||
Genres []*string
|
||||
AverageScoreGreater *int
|
||||
Season *MediaSeason
|
||||
SeasonYear *int
|
||||
Format *MediaFormat
|
||||
IsAdult *bool
|
||||
}{
|
||||
{
|
||||
name: "Popular",
|
||||
Page: lo.ToPtr(1),
|
||||
Search: nil,
|
||||
PerPage: lo.ToPtr(20),
|
||||
Sort: []*MediaSort{lo.ToPtr(MediaSortTrendingDesc)},
|
||||
Status: nil,
|
||||
Genres: nil,
|
||||
AverageScoreGreater: nil,
|
||||
Season: nil,
|
||||
SeasonYear: nil,
|
||||
Format: nil,
|
||||
IsAdult: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
cacheKey := ListAnimeCacheKey(
|
||||
tt.Page,
|
||||
tt.Search,
|
||||
tt.PerPage,
|
||||
tt.Sort,
|
||||
tt.Status,
|
||||
tt.Genres,
|
||||
tt.AverageScoreGreater,
|
||||
tt.Season,
|
||||
tt.SeasonYear,
|
||||
tt.Format,
|
||||
tt.IsAdult,
|
||||
)
|
||||
|
||||
t.Log(cacheKey)
|
||||
|
||||
res, err := ListAnimeM(
|
||||
tt.Page,
|
||||
tt.Search,
|
||||
tt.PerPage,
|
||||
tt.Sort,
|
||||
tt.Status,
|
||||
tt.Genres,
|
||||
tt.AverageScoreGreater,
|
||||
tt.Season,
|
||||
tt.SeasonYear,
|
||||
tt.Format,
|
||||
tt.IsAdult,
|
||||
util.NewLogger(),
|
||||
"",
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, *tt.PerPage, len(res.GetPage().GetMedia()))
|
||||
|
||||
spew.Dump(res)
|
||||
})
|
||||
}
|
||||
}
|
||||
248
seanime-2.9.10/internal/api/anilist/collection_helper.go
Normal file
248
seanime-2.9.10/internal/api/anilist/collection_helper.go
Normal file
@@ -0,0 +1,248 @@
|
||||
package anilist
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
type (
|
||||
AnimeListEntry = AnimeCollection_MediaListCollection_Lists_Entries
|
||||
AnimeList = AnimeCollection_MediaListCollection_Lists
|
||||
|
||||
EntryDate struct {
|
||||
Year *int `json:"year,omitempty"`
|
||||
Month *int `json:"month,omitempty"`
|
||||
Day *int `json:"day,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
func (ac *AnimeCollection) GetListEntryFromAnimeId(id int) (*AnimeListEntry, bool) {
|
||||
if ac == nil || ac.MediaListCollection == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var entry *AnimeCollection_MediaListCollection_Lists_Entries
|
||||
for _, l := range ac.MediaListCollection.Lists {
|
||||
if l.Entries == nil || len(l.Entries) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, e := range l.Entries {
|
||||
if e.Media.ID == id {
|
||||
entry = e
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if entry == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return entry, true
|
||||
}
|
||||
|
||||
func (ac *AnimeCollection) GetAllAnime() []*BaseAnime {
|
||||
if ac == nil {
|
||||
return make([]*BaseAnime, 0)
|
||||
}
|
||||
|
||||
var ret []*BaseAnime
|
||||
addedId := make(map[int]bool)
|
||||
for _, l := range ac.MediaListCollection.Lists {
|
||||
if l.Entries == nil || len(l.Entries) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, e := range l.Entries {
|
||||
if _, ok := addedId[e.Media.ID]; !ok {
|
||||
ret = append(ret, e.Media)
|
||||
addedId[e.Media.ID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (ac *AnimeCollection) FindAnime(mediaId int) (*BaseAnime, bool) {
|
||||
if ac == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
for _, l := range ac.MediaListCollection.Lists {
|
||||
if l.Entries == nil || len(l.Entries) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, e := range l.Entries {
|
||||
if e.Media.ID == mediaId {
|
||||
return e.Media, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (ac *AnimeCollectionWithRelations) GetListEntryFromMediaId(id int) (*AnimeCollectionWithRelations_MediaListCollection_Lists_Entries, bool) {
|
||||
|
||||
if ac == nil || ac.MediaListCollection == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var entry *AnimeCollectionWithRelations_MediaListCollection_Lists_Entries
|
||||
for _, l := range ac.MediaListCollection.Lists {
|
||||
if l.Entries == nil || len(l.Entries) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, e := range l.Entries {
|
||||
if e.Media.ID == id {
|
||||
entry = e
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if entry == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return entry, true
|
||||
}
|
||||
|
||||
func (ac *AnimeCollectionWithRelations) GetAllAnime() []*CompleteAnime {
|
||||
|
||||
var ret []*CompleteAnime
|
||||
addedId := make(map[int]bool)
|
||||
for _, l := range ac.MediaListCollection.Lists {
|
||||
if l.Entries == nil || len(l.Entries) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, e := range l.Entries {
|
||||
if _, ok := addedId[e.Media.ID]; !ok {
|
||||
ret = append(ret, e.Media)
|
||||
addedId[e.Media.ID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (ac *AnimeCollectionWithRelations) FindAnime(mediaId int) (*CompleteAnime, bool) {
|
||||
for _, l := range ac.MediaListCollection.Lists {
|
||||
if l.Entries == nil || len(l.Entries) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, e := range l.Entries {
|
||||
if e.Media.ID == mediaId {
|
||||
return e.Media, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
type IFuzzyDate interface {
|
||||
GetYear() *int
|
||||
GetMonth() *int
|
||||
GetDay() *int
|
||||
}
|
||||
|
||||
func FuzzyDateToString(d IFuzzyDate) string {
|
||||
if d == nil {
|
||||
return ""
|
||||
}
|
||||
return fuzzyDateToString(d.GetYear(), d.GetMonth(), d.GetDay())
|
||||
}
|
||||
|
||||
func ToEntryStartDate(d *AnimeCollection_MediaListCollection_Lists_Entries_StartedAt) string {
|
||||
if d == nil {
|
||||
return ""
|
||||
}
|
||||
return fuzzyDateToString(d.GetYear(), d.GetMonth(), d.GetDay())
|
||||
}
|
||||
|
||||
func ToEntryCompletionDate(d *AnimeCollection_MediaListCollection_Lists_Entries_CompletedAt) string {
|
||||
if d == nil {
|
||||
return ""
|
||||
}
|
||||
return fuzzyDateToString(d.GetYear(), d.GetMonth(), d.GetDay())
|
||||
}
|
||||
|
||||
func fuzzyDateToString(year *int, month *int, day *int) string {
|
||||
_year := 0
|
||||
if year != nil {
|
||||
_year = *year
|
||||
}
|
||||
if _year == 0 {
|
||||
return ""
|
||||
}
|
||||
_month := 0
|
||||
if month != nil {
|
||||
_month = *month
|
||||
}
|
||||
_day := 0
|
||||
if day != nil {
|
||||
_day = *day
|
||||
}
|
||||
return time.Date(_year, time.Month(_month), _day, 0, 0, 0, 0, time.UTC).Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// AddEntryToList adds an entry to the appropriate list based on the provided status.
|
||||
// If no list exists with the given status, a new list is created.
|
||||
func (mc *AnimeCollection_MediaListCollection) AddEntryToList(entry *AnimeCollection_MediaListCollection_Lists_Entries, status MediaListStatus) {
|
||||
if mc == nil || entry == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize Lists slice if nil
|
||||
if mc.Lists == nil {
|
||||
mc.Lists = make([]*AnimeCollection_MediaListCollection_Lists, 0)
|
||||
}
|
||||
|
||||
// Find existing list with the target status
|
||||
for _, list := range mc.Lists {
|
||||
if list.Status != nil && *list.Status == status {
|
||||
// Found the list, add the entry
|
||||
if list.Entries == nil {
|
||||
list.Entries = make([]*AnimeCollection_MediaListCollection_Lists_Entries, 0)
|
||||
}
|
||||
list.Entries = append(list.Entries, entry)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// No list found with the target status, create a new one
|
||||
newList := &AnimeCollection_MediaListCollection_Lists{
|
||||
Status: &status,
|
||||
Entries: []*AnimeCollection_MediaListCollection_Lists_Entries{entry},
|
||||
}
|
||||
mc.Lists = append(mc.Lists, newList)
|
||||
}
|
||||
|
||||
func (ac *AnimeCollection) Copy() *AnimeCollection {
|
||||
if ac == nil {
|
||||
return nil
|
||||
}
|
||||
marshaled, err := json.Marshal(ac)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var copy AnimeCollection
|
||||
err = json.Unmarshal(marshaled, ©)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return ©
|
||||
}
|
||||
|
||||
func (ac *AnimeList) CopyT() *AnimeCollection_MediaListCollection_Lists {
|
||||
if ac == nil {
|
||||
return nil
|
||||
}
|
||||
marshaled, err := json.Marshal(ac)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var copy AnimeCollection_MediaListCollection_Lists
|
||||
err = json.Unmarshal(marshaled, ©)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return ©
|
||||
}
|
||||
115
seanime-2.9.10/internal/api/anilist/compound_query.go
Normal file
115
seanime-2.9.10/internal/api/anilist/compound_query.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package anilist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goccy/go-json"
|
||||
"seanime/internal/util"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func FetchBaseAnimeMap(ids []int) (ret map[int]*BaseAnime, err error) {
|
||||
|
||||
query := fmt.Sprintf(CompoundBaseAnimeDocument, newCompoundQuery(ids))
|
||||
|
||||
requestBody, err := json.Marshal(map[string]interface{}{
|
||||
"query": query,
|
||||
"variables": nil,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := customQuery(requestBody, util.NewLogger())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res map[string]*BaseAnime
|
||||
|
||||
dataB, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(dataB, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret = make(map[int]*BaseAnime)
|
||||
for k, v := range res {
|
||||
id, err := strconv.Atoi(k[1:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret[id] = v
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func newCompoundQuery(ids []int) string {
|
||||
var query string
|
||||
for _, id := range ids {
|
||||
query += fmt.Sprintf(`
|
||||
t%d: Media(id: %d) {
|
||||
...baseAnime
|
||||
}
|
||||
`, id, id)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
const CompoundBaseAnimeDocument = `query CompoundQueryTest {
|
||||
%s
|
||||
}
|
||||
fragment baseAnime on Media {
|
||||
id
|
||||
idMal
|
||||
siteUrl
|
||||
status(version: 2)
|
||||
season
|
||||
type
|
||||
format
|
||||
bannerImage
|
||||
episodes
|
||||
synonyms
|
||||
isAdult
|
||||
countryOfOrigin
|
||||
meanScore
|
||||
description
|
||||
genres
|
||||
duration
|
||||
trailer {
|
||||
id
|
||||
site
|
||||
thumbnail
|
||||
}
|
||||
title {
|
||||
userPreferred
|
||||
romaji
|
||||
english
|
||||
native
|
||||
}
|
||||
coverImage {
|
||||
extraLarge
|
||||
large
|
||||
medium
|
||||
color
|
||||
}
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
nextAiringEpisode {
|
||||
airingAt
|
||||
timeUntilAiring
|
||||
episode
|
||||
}
|
||||
}`
|
||||
95
seanime-2.9.10/internal/api/anilist/compound_query_test.go
Normal file
95
seanime-2.9.10/internal/api/anilist/compound_query_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package anilist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/stretchr/testify/require"
|
||||
"seanime/internal/test_utils"
|
||||
"seanime/internal/util"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCompoundQuery(t *testing.T) {
|
||||
test_utils.InitTestProvider(t)
|
||||
|
||||
var ids = []int{171457, 21}
|
||||
|
||||
query := fmt.Sprintf(compoundQueryFormatTest, newCompoundQuery(ids))
|
||||
|
||||
t.Log(query)
|
||||
|
||||
requestBody, err := json.Marshal(map[string]interface{}{
|
||||
"query": query,
|
||||
"variables": nil,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := customQuery(requestBody, util.NewLogger())
|
||||
require.NoError(t, err)
|
||||
|
||||
var res map[string]*BaseAnime
|
||||
|
||||
dataB, err := json.Marshal(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = json.Unmarshal(dataB, &res)
|
||||
require.NoError(t, err)
|
||||
|
||||
spew.Dump(res)
|
||||
|
||||
}
|
||||
|
||||
const compoundQueryFormatTest = `query CompoundQueryTest {
|
||||
%s
|
||||
}
|
||||
fragment baseAnime on Media {
|
||||
id
|
||||
idMal
|
||||
siteUrl
|
||||
status(version: 2)
|
||||
season
|
||||
type
|
||||
format
|
||||
bannerImage
|
||||
episodes
|
||||
synonyms
|
||||
isAdult
|
||||
countryOfOrigin
|
||||
meanScore
|
||||
description
|
||||
genres
|
||||
duration
|
||||
trailer {
|
||||
id
|
||||
site
|
||||
thumbnail
|
||||
}
|
||||
title {
|
||||
userPreferred
|
||||
romaji
|
||||
english
|
||||
native
|
||||
}
|
||||
coverImage {
|
||||
extraLarge
|
||||
large
|
||||
medium
|
||||
color
|
||||
}
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
nextAiringEpisode {
|
||||
airingAt
|
||||
timeUntilAiring
|
||||
episode
|
||||
}
|
||||
}`
|
||||
140
seanime-2.9.10/internal/api/anilist/custom_query.go
Normal file
140
seanime-2.9.10/internal/api/anilist/custom_query.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package anilist
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"seanime/internal/util"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func CustomQuery(body map[string]interface{}, logger *zerolog.Logger, token string) (data interface{}, err error) {
|
||||
bodyBytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return customQuery(bodyBytes, logger, token)
|
||||
}
|
||||
|
||||
func customQuery(body []byte, logger *zerolog.Logger, token ...string) (data interface{}, err error) {
|
||||
|
||||
var rlRemainingStr string
|
||||
|
||||
reqTime := time.Now()
|
||||
defer func() {
|
||||
timeSince := time.Since(reqTime)
|
||||
formattedDur := timeSince.Truncate(time.Millisecond).String()
|
||||
if err != nil {
|
||||
logger.Error().Str("duration", formattedDur).Str("rlr", rlRemainingStr).Err(err).Msg("anilist: Failed Request")
|
||||
} else {
|
||||
if timeSince > 600*time.Millisecond {
|
||||
logger.Warn().Str("rtt", formattedDur).Str("rlr", rlRemainingStr).Msg("anilist: Long Request")
|
||||
} else {
|
||||
logger.Trace().Str("rtt", formattedDur).Str("rlr", rlRemainingStr).Msg("anilist: Successful Request")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
defer util.HandlePanicInModuleThen("api/anilist/custom_query", func() {
|
||||
err = errors.New("panic in customQuery")
|
||||
})
|
||||
|
||||
client := http.DefaultClient
|
||||
|
||||
var req *http.Request
|
||||
req, err = http.NewRequest("POST", "https://graphql.anilist.co", bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if len(token) > 0 && token[0] != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token[0]))
|
||||
}
|
||||
|
||||
// Send request
|
||||
retryCount := 2
|
||||
|
||||
var resp *http.Response
|
||||
for i := 0; i < retryCount; i++ {
|
||||
|
||||
// Reset response body for retry
|
||||
if resp != nil && resp.Body != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
// Recreate the request body if it was read in a previous attempt
|
||||
if req.GetBody != nil {
|
||||
newBody, err := req.GetBody()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get request body: %w", err)
|
||||
}
|
||||
req.Body = newBody
|
||||
}
|
||||
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
|
||||
rlRemainingStr = resp.Header.Get("X-Ratelimit-Remaining")
|
||||
rlRetryAfterStr := resp.Header.Get("Retry-After")
|
||||
rlRetryAfter, err := strconv.Atoi(rlRetryAfterStr)
|
||||
if err == nil {
|
||||
logger.Warn().Msgf("anilist: Rate limited, retrying in %d seconds", rlRetryAfter+1)
|
||||
select {
|
||||
case <-time.After(time.Duration(rlRetryAfter+1) * time.Second):
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if rlRemainingStr == "" {
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.Header.Get("Content-Encoding") == "gzip" {
|
||||
resp.Body, err = gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gzip decode failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var res interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&res)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
var ok bool
|
||||
|
||||
reqErrors, ok := res.(map[string]interface{})["errors"].([]interface{})
|
||||
|
||||
if ok && len(reqErrors) > 0 {
|
||||
firstError, foundErr := reqErrors[0].(map[string]interface{})
|
||||
if foundErr {
|
||||
return nil, errors.New(firstError["message"].(string))
|
||||
}
|
||||
}
|
||||
|
||||
data, ok = res.(map[string]interface{})["data"]
|
||||
if !ok {
|
||||
return nil, errors.New("failed to parse data")
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
27
seanime-2.9.10/internal/api/anilist/date_test.go
Normal file
27
seanime-2.9.10/internal/api/anilist/date_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package anilist
|
||||
|
||||
//import (
|
||||
|
||||
//)
|
||||
//
|
||||
//func TestFuzzyDate(t *testing.T) {
|
||||
//
|
||||
// date := "2006-01-02T15:04:05Z"
|
||||
//
|
||||
// parsedDate, err := time.Parse(time.RFC3339, date)
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// year := parsedDate.Year()
|
||||
// month := int(parsedDate.Month())
|
||||
// day := parsedDate.Day()
|
||||
// t.Logf("Year: %d, Month: %d, Day: %d", year, month, day)
|
||||
//
|
||||
//}
|
||||
//
|
||||
//func TestDateTransformation(t *testing.T) {
|
||||
//
|
||||
// t.Logf(time.Date(2024, time.Month(1), 1, 0, 0, 0, 0, time.Local).UTC().Format(time.RFC3339))
|
||||
//
|
||||
//}
|
||||
50
seanime-2.9.10/internal/api/anilist/entries.go
Normal file
50
seanime-2.9.10/internal/api/anilist/entries.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package anilist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/rs/zerolog"
|
||||
"seanime/internal/util/limiter"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func (c *Client) AddMediaToPlanning(mIds []int, rateLimiter *limiter.Limiter, logger *zerolog.Logger) error {
|
||||
if len(mIds) == 0 {
|
||||
logger.Debug().Msg("anilist: No media added to planning list")
|
||||
return nil
|
||||
}
|
||||
if rateLimiter == nil {
|
||||
return errors.New("anilist: no rate limiter provided")
|
||||
}
|
||||
|
||||
status := MediaListStatusPlanning
|
||||
|
||||
scoreRaw := 0
|
||||
progress := 0
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
for _, _id := range mIds {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
rateLimiter.Wait()
|
||||
defer wg.Done()
|
||||
_, err := c.UpdateMediaListEntry(
|
||||
context.Background(),
|
||||
&id,
|
||||
&status,
|
||||
&scoreRaw,
|
||||
&progress,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error().Msg("anilist: An error occurred while adding media to planning list: " + err.Error())
|
||||
}
|
||||
}(_id)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
logger.Debug().Any("count", len(mIds)).Msg("anilist: Media added to planning list")
|
||||
|
||||
return nil
|
||||
}
|
||||
19
seanime-2.9.10/internal/api/anilist/hook_events.go
Normal file
19
seanime-2.9.10/internal/api/anilist/hook_events.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package anilist
|
||||
|
||||
import "seanime/internal/hook_resolver"
|
||||
|
||||
// ListMissedSequelsRequestedEvent is triggered when the list missed sequels request is requested.
|
||||
// Prevent default to skip the default behavior and return your own data.
|
||||
type ListMissedSequelsRequestedEvent struct {
|
||||
hook_resolver.Event
|
||||
AnimeCollectionWithRelations *AnimeCollectionWithRelations `json:"animeCollectionWithRelations"`
|
||||
Variables map[string]interface{} `json:"variables"`
|
||||
Query string `json:"query"`
|
||||
// Empty data object, will be used if the hook prevents the default behavior
|
||||
List []*BaseAnime `json:"list"`
|
||||
}
|
||||
|
||||
type ListMissedSequelsEvent struct {
|
||||
hook_resolver.Event
|
||||
List []*BaseAnime `json:"list"`
|
||||
}
|
||||
529
seanime-2.9.10/internal/api/anilist/list.go
Normal file
529
seanime-2.9.10/internal/api/anilist/list.go
Normal file
@@ -0,0 +1,529 @@
|
||||
package anilist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"seanime/internal/hook"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func ListMissedSequels(
|
||||
animeCollectionWithRelations *AnimeCollectionWithRelations,
|
||||
logger *zerolog.Logger,
|
||||
token string,
|
||||
) (ret []*BaseAnime, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
variables := map[string]interface{}{}
|
||||
variables["page"] = 1
|
||||
variables["perPage"] = 50
|
||||
|
||||
ids := make(map[int]struct{})
|
||||
for _, list := range animeCollectionWithRelations.GetMediaListCollection().GetLists() {
|
||||
if list.Status == nil || !(*list.Status == MediaListStatusCompleted || *list.Status == MediaListStatusRepeating || *list.Status == MediaListStatusPaused) || list.Entries == nil {
|
||||
continue
|
||||
}
|
||||
for _, entry := range list.Entries {
|
||||
if _, ok := ids[entry.GetMedia().GetID()]; !ok {
|
||||
edges := entry.GetMedia().GetRelations().GetEdges()
|
||||
var sequel *BaseAnime
|
||||
for _, edge := range edges {
|
||||
if edge.GetRelationType() != nil && *edge.GetRelationType() == MediaRelationSequel {
|
||||
sequel = edge.GetNode()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if sequel == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if sequel is already in the list
|
||||
_, found := animeCollectionWithRelations.FindAnime(sequel.GetID())
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
|
||||
if *sequel.GetStatus() == MediaStatusFinished || *sequel.GetStatus() == MediaStatusReleasing {
|
||||
ids[sequel.GetID()] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
idsSlice := make([]int, 0, len(ids))
|
||||
for id := range ids {
|
||||
idsSlice = append(idsSlice, id)
|
||||
}
|
||||
|
||||
if len(idsSlice) == 0 {
|
||||
return []*BaseAnime{}, nil
|
||||
}
|
||||
|
||||
if len(idsSlice) > 10 {
|
||||
idsSlice = idsSlice[:10]
|
||||
}
|
||||
|
||||
variables["ids"] = idsSlice
|
||||
variables["inCollection"] = false
|
||||
variables["sort"] = MediaSortStartDateDesc
|
||||
|
||||
// Event
|
||||
reqEvent := &ListMissedSequelsRequestedEvent{
|
||||
AnimeCollectionWithRelations: animeCollectionWithRelations,
|
||||
Variables: variables,
|
||||
List: make([]*BaseAnime, 0),
|
||||
Query: SearchBaseAnimeByIdsDocument,
|
||||
}
|
||||
err = hook.GlobalHookManager.OnListMissedSequelsRequested().Trigger(reqEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the hook prevented the default behavior, return the data
|
||||
if reqEvent.DefaultPrevented {
|
||||
return reqEvent.List, nil
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(map[string]interface{}{
|
||||
"query": reqEvent.Query,
|
||||
"variables": reqEvent.Variables,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := customQuery(requestBody, logger, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var searchRes *SearchBaseAnimeByIds
|
||||
if err := json.Unmarshal(m, &searchRes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if searchRes == nil || searchRes.Page == nil || searchRes.Page.Media == nil {
|
||||
return nil, fmt.Errorf("no data found")
|
||||
}
|
||||
|
||||
// Event
|
||||
event := &ListMissedSequelsEvent{
|
||||
List: searchRes.Page.Media,
|
||||
}
|
||||
err = hook.GlobalHookManager.OnListMissedSequels().Trigger(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return event.List, nil
|
||||
}
|
||||
|
||||
func ListAnimeM(
|
||||
Page *int,
|
||||
Search *string,
|
||||
PerPage *int,
|
||||
Sort []*MediaSort,
|
||||
Status []*MediaStatus,
|
||||
Genres []*string,
|
||||
AverageScoreGreater *int,
|
||||
Season *MediaSeason,
|
||||
SeasonYear *int,
|
||||
Format *MediaFormat,
|
||||
IsAdult *bool,
|
||||
logger *zerolog.Logger,
|
||||
token string,
|
||||
) (*ListAnime, error) {
|
||||
|
||||
variables := map[string]interface{}{}
|
||||
if Page != nil {
|
||||
variables["page"] = *Page
|
||||
}
|
||||
if Search != nil {
|
||||
variables["search"] = *Search
|
||||
}
|
||||
if PerPage != nil {
|
||||
variables["perPage"] = *PerPage
|
||||
}
|
||||
if Sort != nil {
|
||||
variables["sort"] = Sort
|
||||
}
|
||||
if Status != nil {
|
||||
variables["status"] = Status
|
||||
}
|
||||
if Genres != nil {
|
||||
variables["genres"] = Genres
|
||||
}
|
||||
if AverageScoreGreater != nil {
|
||||
variables["averageScore_greater"] = *AverageScoreGreater
|
||||
}
|
||||
if Season != nil {
|
||||
variables["season"] = *Season
|
||||
}
|
||||
if SeasonYear != nil {
|
||||
variables["seasonYear"] = *SeasonYear
|
||||
}
|
||||
if Format != nil {
|
||||
variables["format"] = *Format
|
||||
}
|
||||
if IsAdult != nil {
|
||||
variables["isAdult"] = *IsAdult
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(map[string]interface{}{
|
||||
"query": ListAnimeDocument,
|
||||
"variables": variables,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := customQuery(requestBody, logger, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var listMediaF ListAnime
|
||||
m, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(m, &listMediaF); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &listMediaF, nil
|
||||
}
|
||||
|
||||
func ListMangaM(
|
||||
Page *int,
|
||||
Search *string,
|
||||
PerPage *int,
|
||||
Sort []*MediaSort,
|
||||
Status []*MediaStatus,
|
||||
Genres []*string,
|
||||
AverageScoreGreater *int,
|
||||
Year *int,
|
||||
Format *MediaFormat,
|
||||
CountryOfOrigin *string,
|
||||
IsAdult *bool,
|
||||
logger *zerolog.Logger,
|
||||
token string,
|
||||
) (*ListManga, error) {
|
||||
|
||||
variables := map[string]interface{}{}
|
||||
if Page != nil {
|
||||
variables["page"] = *Page
|
||||
}
|
||||
if Search != nil {
|
||||
variables["search"] = *Search
|
||||
}
|
||||
if PerPage != nil {
|
||||
variables["perPage"] = *PerPage
|
||||
}
|
||||
if Sort != nil {
|
||||
variables["sort"] = Sort
|
||||
}
|
||||
if Status != nil {
|
||||
variables["status"] = Status
|
||||
}
|
||||
if Genres != nil {
|
||||
variables["genres"] = Genres
|
||||
}
|
||||
if AverageScoreGreater != nil {
|
||||
variables["averageScore_greater"] = *AverageScoreGreater * 10
|
||||
}
|
||||
if Year != nil {
|
||||
variables["startDate_greater"] = lo.ToPtr(fmt.Sprintf("%d0000", *Year))
|
||||
variables["startDate_lesser"] = lo.ToPtr(fmt.Sprintf("%d0000", *Year+1))
|
||||
}
|
||||
if Format != nil {
|
||||
variables["format"] = *Format
|
||||
}
|
||||
if CountryOfOrigin != nil {
|
||||
variables["countryOfOrigin"] = *CountryOfOrigin
|
||||
}
|
||||
if IsAdult != nil {
|
||||
variables["isAdult"] = *IsAdult
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(map[string]interface{}{
|
||||
"query": ListMangaDocument,
|
||||
"variables": variables,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := customQuery(requestBody, logger, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var listMediaF ListManga
|
||||
m, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(m, &listMediaF); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &listMediaF, nil
|
||||
}
|
||||
|
||||
func ListRecentAiringAnimeM(
|
||||
Page *int,
|
||||
Search *string,
|
||||
PerPage *int,
|
||||
AiringAtGreater *int,
|
||||
AiringAtLesser *int,
|
||||
NotYetAired *bool,
|
||||
Sort []*AiringSort,
|
||||
logger *zerolog.Logger,
|
||||
token string,
|
||||
) (*ListRecentAnime, error) {
|
||||
|
||||
variables := map[string]interface{}{}
|
||||
if Page != nil {
|
||||
variables["page"] = *Page
|
||||
}
|
||||
if Search != nil {
|
||||
variables["search"] = *Search
|
||||
}
|
||||
if PerPage != nil {
|
||||
variables["perPage"] = *PerPage
|
||||
}
|
||||
if AiringAtGreater != nil {
|
||||
variables["airingAt_greater"] = *AiringAtGreater
|
||||
}
|
||||
if AiringAtLesser != nil {
|
||||
variables["airingAt_lesser"] = *AiringAtLesser
|
||||
}
|
||||
if NotYetAired != nil {
|
||||
variables["notYetAired"] = *NotYetAired
|
||||
}
|
||||
if Sort != nil {
|
||||
variables["sort"] = Sort
|
||||
} else {
|
||||
variables["sort"] = []*AiringSort{lo.ToPtr(AiringSortTimeDesc)}
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(map[string]interface{}{
|
||||
"query": ListRecentAiringAnimeQuery,
|
||||
"variables": variables,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := customQuery(requestBody, logger, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var listMediaF ListRecentAnime
|
||||
m, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(m, &listMediaF); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &listMediaF, nil
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func ListAnimeCacheKey(
|
||||
Page *int,
|
||||
Search *string,
|
||||
PerPage *int,
|
||||
Sort []*MediaSort,
|
||||
Status []*MediaStatus,
|
||||
Genres []*string,
|
||||
AverageScoreGreater *int,
|
||||
Season *MediaSeason,
|
||||
SeasonYear *int,
|
||||
Format *MediaFormat,
|
||||
IsAdult *bool,
|
||||
) string {
|
||||
|
||||
key := "ListAnime"
|
||||
if Page != nil {
|
||||
key += fmt.Sprintf("_%d", *Page)
|
||||
}
|
||||
if Search != nil {
|
||||
key += fmt.Sprintf("_%s", *Search)
|
||||
}
|
||||
if PerPage != nil {
|
||||
key += fmt.Sprintf("_%d", *PerPage)
|
||||
}
|
||||
if Sort != nil {
|
||||
key += fmt.Sprintf("_%v", Sort)
|
||||
}
|
||||
if Status != nil {
|
||||
key += fmt.Sprintf("_%v", Status)
|
||||
}
|
||||
if Genres != nil {
|
||||
key += fmt.Sprintf("_%v", Genres)
|
||||
}
|
||||
if AverageScoreGreater != nil {
|
||||
key += fmt.Sprintf("_%d", *AverageScoreGreater)
|
||||
}
|
||||
if Season != nil {
|
||||
key += fmt.Sprintf("_%s", *Season)
|
||||
}
|
||||
if SeasonYear != nil {
|
||||
key += fmt.Sprintf("_%d", *SeasonYear)
|
||||
}
|
||||
if Format != nil {
|
||||
key += fmt.Sprintf("_%s", *Format)
|
||||
}
|
||||
if IsAdult != nil {
|
||||
key += fmt.Sprintf("_%t", *IsAdult)
|
||||
}
|
||||
|
||||
return key
|
||||
|
||||
}
|
||||
func ListMangaCacheKey(
|
||||
Page *int,
|
||||
Search *string,
|
||||
PerPage *int,
|
||||
Sort []*MediaSort,
|
||||
Status []*MediaStatus,
|
||||
Genres []*string,
|
||||
AverageScoreGreater *int,
|
||||
Season *MediaSeason,
|
||||
SeasonYear *int,
|
||||
Format *MediaFormat,
|
||||
CountryOfOrigin *string,
|
||||
IsAdult *bool,
|
||||
) string {
|
||||
|
||||
key := "ListAnime"
|
||||
if Page != nil {
|
||||
key += fmt.Sprintf("_%d", *Page)
|
||||
}
|
||||
if Search != nil {
|
||||
key += fmt.Sprintf("_%s", *Search)
|
||||
}
|
||||
if PerPage != nil {
|
||||
key += fmt.Sprintf("_%d", *PerPage)
|
||||
}
|
||||
if Sort != nil {
|
||||
key += fmt.Sprintf("_%v", Sort)
|
||||
}
|
||||
if Status != nil {
|
||||
key += fmt.Sprintf("_%v", Status)
|
||||
}
|
||||
if Genres != nil {
|
||||
key += fmt.Sprintf("_%v", Genres)
|
||||
}
|
||||
if AverageScoreGreater != nil {
|
||||
key += fmt.Sprintf("_%d", *AverageScoreGreater)
|
||||
}
|
||||
if Season != nil {
|
||||
key += fmt.Sprintf("_%s", *Season)
|
||||
}
|
||||
if SeasonYear != nil {
|
||||
key += fmt.Sprintf("_%d", *SeasonYear)
|
||||
}
|
||||
if Format != nil {
|
||||
key += fmt.Sprintf("_%s", *Format)
|
||||
}
|
||||
if CountryOfOrigin != nil {
|
||||
key += fmt.Sprintf("_%s", *CountryOfOrigin)
|
||||
}
|
||||
if IsAdult != nil {
|
||||
key += fmt.Sprintf("_%t", *IsAdult)
|
||||
}
|
||||
|
||||
return key
|
||||
|
||||
}
|
||||
|
||||
const ListRecentAiringAnimeQuery = `query ListRecentAnime ($page: Int, $perPage: Int, $airingAt_greater: Int, $airingAt_lesser: Int, $sort: [AiringSort], $notYetAired: Boolean = false) {
|
||||
Page(page: $page, perPage: $perPage) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
total
|
||||
perPage
|
||||
currentPage
|
||||
lastPage
|
||||
}
|
||||
airingSchedules(notYetAired: $notYetAired, sort: $sort, airingAt_greater: $airingAt_greater, airingAt_lesser: $airingAt_lesser) {
|
||||
id
|
||||
airingAt
|
||||
episode
|
||||
timeUntilAiring
|
||||
media {
|
||||
... baseAnime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fragment baseAnime on Media {
|
||||
id
|
||||
idMal
|
||||
siteUrl
|
||||
status(version: 2)
|
||||
season
|
||||
type
|
||||
format
|
||||
bannerImage
|
||||
episodes
|
||||
synonyms
|
||||
isAdult
|
||||
countryOfOrigin
|
||||
meanScore
|
||||
description
|
||||
genres
|
||||
duration
|
||||
trailer {
|
||||
id
|
||||
site
|
||||
thumbnail
|
||||
}
|
||||
title {
|
||||
userPreferred
|
||||
romaji
|
||||
english
|
||||
native
|
||||
}
|
||||
coverImage {
|
||||
extraLarge
|
||||
large
|
||||
medium
|
||||
color
|
||||
}
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
nextAiringEpisode {
|
||||
airingAt
|
||||
timeUntilAiring
|
||||
episode
|
||||
}
|
||||
}
|
||||
`
|
||||
123
seanime-2.9.10/internal/api/anilist/manga.go
Normal file
123
seanime-2.9.10/internal/api/anilist/manga.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package anilist
|
||||
|
||||
type MangaList = MangaCollection_MediaListCollection_Lists
|
||||
type MangaListEntry = MangaCollection_MediaListCollection_Lists_Entries
|
||||
|
||||
func (ac *MangaCollection) GetListEntryFromMangaId(id int) (*MangaListEntry, bool) {
|
||||
|
||||
if ac == nil || ac.MediaListCollection == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var entry *MangaCollection_MediaListCollection_Lists_Entries
|
||||
for _, l := range ac.MediaListCollection.Lists {
|
||||
if l.Entries == nil || len(l.Entries) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, e := range l.Entries {
|
||||
if e.Media.ID == id {
|
||||
entry = e
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if entry == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return entry, true
|
||||
}
|
||||
|
||||
func (m *BaseManga) GetTitleSafe() string {
|
||||
if m.GetTitle().GetEnglish() != nil {
|
||||
return *m.GetTitle().GetEnglish()
|
||||
}
|
||||
if m.GetTitle().GetRomaji() != nil {
|
||||
return *m.GetTitle().GetRomaji()
|
||||
}
|
||||
return "N/A"
|
||||
}
|
||||
func (m *BaseManga) GetRomajiTitleSafe() string {
|
||||
if m.GetTitle().GetRomaji() != nil {
|
||||
return *m.GetTitle().GetRomaji()
|
||||
}
|
||||
if m.GetTitle().GetEnglish() != nil {
|
||||
return *m.GetTitle().GetEnglish()
|
||||
}
|
||||
return "N/A"
|
||||
}
|
||||
|
||||
func (m *BaseManga) GetPreferredTitle() string {
|
||||
if m.GetTitle().GetUserPreferred() != nil {
|
||||
return *m.GetTitle().GetUserPreferred()
|
||||
}
|
||||
return m.GetTitleSafe()
|
||||
}
|
||||
|
||||
func (m *BaseManga) GetCoverImageSafe() string {
|
||||
if m.GetCoverImage().GetExtraLarge() != nil {
|
||||
return *m.GetCoverImage().GetExtraLarge()
|
||||
}
|
||||
if m.GetCoverImage().GetLarge() != nil {
|
||||
return *m.GetCoverImage().GetLarge()
|
||||
}
|
||||
if m.GetBannerImage() != nil {
|
||||
return *m.GetBannerImage()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (m *BaseManga) GetBannerImageSafe() string {
|
||||
if m.GetBannerImage() != nil {
|
||||
return *m.GetBannerImage()
|
||||
}
|
||||
return m.GetCoverImageSafe()
|
||||
}
|
||||
|
||||
func (m *BaseManga) GetAllTitles() []*string {
|
||||
titles := make([]*string, 0)
|
||||
if m.HasRomajiTitle() {
|
||||
titles = append(titles, m.Title.Romaji)
|
||||
}
|
||||
if m.HasEnglishTitle() {
|
||||
titles = append(titles, m.Title.English)
|
||||
}
|
||||
if m.HasSynonyms() && len(m.Synonyms) > 1 {
|
||||
titles = append(titles, m.Synonyms...)
|
||||
}
|
||||
return titles
|
||||
}
|
||||
|
||||
func (m *BaseManga) GetMainTitlesDeref() []string {
|
||||
titles := make([]string, 0)
|
||||
if m.HasRomajiTitle() {
|
||||
titles = append(titles, *m.Title.Romaji)
|
||||
}
|
||||
if m.HasEnglishTitle() {
|
||||
titles = append(titles, *m.Title.English)
|
||||
}
|
||||
return titles
|
||||
}
|
||||
|
||||
func (m *BaseManga) HasEnglishTitle() bool {
|
||||
return m.Title.English != nil
|
||||
}
|
||||
func (m *BaseManga) HasRomajiTitle() bool {
|
||||
return m.Title.Romaji != nil
|
||||
}
|
||||
func (m *BaseManga) HasSynonyms() bool {
|
||||
return m.Synonyms != nil
|
||||
}
|
||||
|
||||
func (m *BaseManga) GetStartYearSafe() int {
|
||||
if m.GetStartDate() != nil && m.GetStartDate().GetYear() != nil {
|
||||
return *m.GetStartDate().GetYear()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *MangaListEntry) GetRepeatSafe() int {
|
||||
if m.Repeat == nil {
|
||||
return 0
|
||||
}
|
||||
return *m.Repeat
|
||||
}
|
||||
25
seanime-2.9.10/internal/api/anilist/media.go
Normal file
25
seanime-2.9.10/internal/api/anilist/media.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package anilist
|
||||
|
||||
import (
|
||||
"seanime/internal/util/result"
|
||||
)
|
||||
|
||||
type BaseAnimeCache struct {
|
||||
*result.Cache[int, *BaseAnime]
|
||||
}
|
||||
|
||||
// NewBaseAnimeCache returns a new result.Cache[int, *BaseAnime].
|
||||
// It is used to temporarily store the results of FetchMediaTree calls.
|
||||
func NewBaseAnimeCache() *BaseAnimeCache {
|
||||
return &BaseAnimeCache{result.NewCache[int, *BaseAnime]()}
|
||||
}
|
||||
|
||||
type CompleteAnimeCache struct {
|
||||
*result.Cache[int, *CompleteAnime]
|
||||
}
|
||||
|
||||
// NewCompleteAnimeCache returns a new result.Cache[int, *CompleteAnime].
|
||||
// It is used to temporarily store the results of FetchMediaTree calls.
|
||||
func NewCompleteAnimeCache() *CompleteAnimeCache {
|
||||
return &CompleteAnimeCache{result.NewCache[int, *CompleteAnime]()}
|
||||
}
|
||||
574
seanime-2.9.10/internal/api/anilist/media_helper.go
Normal file
574
seanime-2.9.10/internal/api/anilist/media_helper.go
Normal file
@@ -0,0 +1,574 @@
|
||||
package anilist
|
||||
|
||||
import (
|
||||
"seanime/internal/util/comparison"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func (m *BaseAnime) GetTitleSafe() string {
|
||||
if m.GetTitle().GetEnglish() != nil {
|
||||
return *m.GetTitle().GetEnglish()
|
||||
}
|
||||
if m.GetTitle().GetRomaji() != nil {
|
||||
return *m.GetTitle().GetRomaji()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *BaseAnime) GetEnglishTitleSafe() string {
|
||||
if m.GetTitle().GetEnglish() != nil {
|
||||
return *m.GetTitle().GetEnglish()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *BaseAnime) GetRomajiTitleSafe() string {
|
||||
if m.GetTitle().GetRomaji() != nil {
|
||||
return *m.GetTitle().GetRomaji()
|
||||
}
|
||||
if m.GetTitle().GetEnglish() != nil {
|
||||
return *m.GetTitle().GetEnglish()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *BaseAnime) GetPreferredTitle() string {
|
||||
if m.GetTitle().GetUserPreferred() != nil {
|
||||
return *m.GetTitle().GetUserPreferred()
|
||||
}
|
||||
return m.GetTitleSafe()
|
||||
}
|
||||
|
||||
func (m *BaseAnime) GetCoverImageSafe() string {
|
||||
if m.GetCoverImage().GetExtraLarge() != nil {
|
||||
return *m.GetCoverImage().GetExtraLarge()
|
||||
}
|
||||
if m.GetCoverImage().GetLarge() != nil {
|
||||
return *m.GetCoverImage().GetLarge()
|
||||
}
|
||||
if m.GetBannerImage() != nil {
|
||||
return *m.GetBannerImage()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *BaseAnime) GetBannerImageSafe() string {
|
||||
if m.GetBannerImage() != nil {
|
||||
return *m.GetBannerImage()
|
||||
}
|
||||
return m.GetCoverImageSafe()
|
||||
}
|
||||
|
||||
func (m *BaseAnime) IsMovieOrSingleEpisode() bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
if m.GetTotalEpisodeCount() == 1 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *BaseAnime) GetSynonymsDeref() []string {
|
||||
if m.Synonyms == nil {
|
||||
return nil
|
||||
}
|
||||
return lo.Map(m.Synonyms, func(s *string, i int) string { return *s })
|
||||
}
|
||||
|
||||
func (m *BaseAnime) GetSynonymsContainingSeason() []string {
|
||||
if m.Synonyms == nil {
|
||||
return nil
|
||||
}
|
||||
return lo.Filter(lo.Map(m.Synonyms, func(s *string, i int) string { return *s }), func(s string, i int) bool { return comparison.ValueContainsSeason(s) })
|
||||
}
|
||||
|
||||
func (m *BaseAnime) GetStartYearSafe() int {
|
||||
if m == nil || m.StartDate == nil || m.StartDate.Year == nil {
|
||||
return 0
|
||||
}
|
||||
return *m.StartDate.Year
|
||||
}
|
||||
|
||||
func (m *BaseAnime) IsMovie() bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
if m.Format == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return *m.Format == MediaFormatMovie
|
||||
}
|
||||
|
||||
func (m *BaseAnime) IsFinished() bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
if m.Status == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return *m.Status == MediaStatusFinished
|
||||
}
|
||||
|
||||
func (m *BaseAnime) GetAllTitles() []*string {
|
||||
titles := make([]*string, 0)
|
||||
if m.HasRomajiTitle() {
|
||||
titles = append(titles, m.Title.Romaji)
|
||||
}
|
||||
if m.HasEnglishTitle() {
|
||||
titles = append(titles, m.Title.English)
|
||||
}
|
||||
if m.HasSynonyms() && len(m.Synonyms) > 1 {
|
||||
titles = append(titles, lo.Filter(m.Synonyms, func(s *string, i int) bool { return comparison.ValueContainsSeason(*s) })...)
|
||||
}
|
||||
return titles
|
||||
}
|
||||
|
||||
func (m *BaseAnime) GetAllTitlesDeref() []string {
|
||||
titles := make([]string, 0)
|
||||
if m.HasRomajiTitle() {
|
||||
titles = append(titles, *m.Title.Romaji)
|
||||
}
|
||||
if m.HasEnglishTitle() {
|
||||
titles = append(titles, *m.Title.English)
|
||||
}
|
||||
if m.HasSynonyms() && len(m.Synonyms) > 1 {
|
||||
syn := lo.Filter(m.Synonyms, func(s *string, i int) bool { return comparison.ValueContainsSeason(*s) })
|
||||
for _, s := range syn {
|
||||
titles = append(titles, *s)
|
||||
}
|
||||
}
|
||||
return titles
|
||||
}
|
||||
|
||||
func (m *BaseAnime) GetMainTitles() []*string {
|
||||
titles := make([]*string, 0)
|
||||
if m.HasRomajiTitle() {
|
||||
titles = append(titles, m.Title.Romaji)
|
||||
}
|
||||
if m.HasEnglishTitle() {
|
||||
titles = append(titles, m.Title.English)
|
||||
}
|
||||
return titles
|
||||
}
|
||||
|
||||
func (m *BaseAnime) GetMainTitlesDeref() []string {
|
||||
titles := make([]string, 0)
|
||||
if m.HasRomajiTitle() {
|
||||
titles = append(titles, *m.Title.Romaji)
|
||||
}
|
||||
if m.HasEnglishTitle() {
|
||||
titles = append(titles, *m.Title.English)
|
||||
}
|
||||
return titles
|
||||
}
|
||||
|
||||
// GetCurrentEpisodeCount returns the current episode number for that media and -1 if it doesn't have one.
|
||||
// i.e. -1 is returned if the media has no episodes AND the next airing episode is not set.
|
||||
func (m *BaseAnime) GetCurrentEpisodeCount() int {
|
||||
ceil := -1
|
||||
if m.Episodes != nil {
|
||||
ceil = *m.Episodes
|
||||
}
|
||||
if m.NextAiringEpisode != nil {
|
||||
if m.NextAiringEpisode.Episode > 0 {
|
||||
ceil = m.NextAiringEpisode.Episode - 1
|
||||
}
|
||||
}
|
||||
|
||||
return ceil
|
||||
}
|
||||
func (m *BaseAnime) GetCurrentEpisodeCountOrNil() *int {
|
||||
n := m.GetCurrentEpisodeCount()
|
||||
if n == -1 {
|
||||
return nil
|
||||
}
|
||||
return &n
|
||||
}
|
||||
|
||||
// GetTotalEpisodeCount returns the total episode number for that media and -1 if it doesn't have one
|
||||
func (m *BaseAnime) GetTotalEpisodeCount() int {
|
||||
ceil := -1
|
||||
if m.Episodes != nil {
|
||||
ceil = *m.Episodes
|
||||
}
|
||||
return ceil
|
||||
}
|
||||
|
||||
// GetTotalEpisodeCount returns the total episode number for that media and -1 if it doesn't have one
|
||||
func (m *BaseAnime) GetTotalEpisodeCountOrNil() *int {
|
||||
return m.Episodes
|
||||
}
|
||||
|
||||
// GetPossibleSeasonNumber returns the possible season number for that media and -1 if it doesn't have one.
|
||||
// It looks at the synonyms and returns the highest season number found.
|
||||
func (m *BaseAnime) GetPossibleSeasonNumber() int {
|
||||
if m == nil || m.Synonyms == nil || len(m.Synonyms) == 0 {
|
||||
return -1
|
||||
}
|
||||
titles := lo.Filter(m.Synonyms, func(s *string, i int) bool { return comparison.ValueContainsSeason(*s) })
|
||||
if m.HasEnglishTitle() {
|
||||
titles = append(titles, m.Title.English)
|
||||
}
|
||||
if m.HasRomajiTitle() {
|
||||
titles = append(titles, m.Title.Romaji)
|
||||
}
|
||||
seasons := lo.Map(titles, func(s *string, i int) int { return comparison.ExtractSeasonNumber(*s) })
|
||||
return lo.Max(seasons)
|
||||
}
|
||||
|
||||
func (m *BaseAnime) HasEnglishTitle() bool {
|
||||
return m.Title.English != nil
|
||||
}
|
||||
|
||||
func (m *BaseAnime) HasRomajiTitle() bool {
|
||||
return m.Title.Romaji != nil
|
||||
}
|
||||
|
||||
func (m *BaseAnime) HasSynonyms() bool {
|
||||
return m.Synonyms != nil
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func (m *CompleteAnime) GetTitleSafe() string {
|
||||
if m.GetTitle().GetEnglish() != nil {
|
||||
return *m.GetTitle().GetEnglish()
|
||||
}
|
||||
if m.GetTitle().GetRomaji() != nil {
|
||||
return *m.GetTitle().GetRomaji()
|
||||
}
|
||||
return "N/A"
|
||||
}
|
||||
func (m *CompleteAnime) GetRomajiTitleSafe() string {
|
||||
if m.GetTitle().GetRomaji() != nil {
|
||||
return *m.GetTitle().GetRomaji()
|
||||
}
|
||||
if m.GetTitle().GetEnglish() != nil {
|
||||
return *m.GetTitle().GetEnglish()
|
||||
}
|
||||
return "N/A"
|
||||
}
|
||||
|
||||
func (m *CompleteAnime) GetPreferredTitle() string {
|
||||
if m.GetTitle().GetUserPreferred() != nil {
|
||||
return *m.GetTitle().GetUserPreferred()
|
||||
}
|
||||
return m.GetTitleSafe()
|
||||
}
|
||||
|
||||
func (m *CompleteAnime) GetCoverImageSafe() string {
|
||||
if m.GetCoverImage().GetExtraLarge() != nil {
|
||||
return *m.GetCoverImage().GetExtraLarge()
|
||||
}
|
||||
if m.GetCoverImage().GetLarge() != nil {
|
||||
return *m.GetCoverImage().GetLarge()
|
||||
}
|
||||
if m.GetBannerImage() != nil {
|
||||
return *m.GetBannerImage()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *CompleteAnime) GetBannerImageSafe() string {
|
||||
if m.GetBannerImage() != nil {
|
||||
return *m.GetBannerImage()
|
||||
}
|
||||
return m.GetCoverImageSafe()
|
||||
}
|
||||
|
||||
func (m *CompleteAnime) IsMovieOrSingleEpisode() bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
if m.GetTotalEpisodeCount() == 1 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *CompleteAnime) IsMovie() bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
if m.Format == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return *m.Format == MediaFormatMovie
|
||||
}
|
||||
|
||||
func (m *CompleteAnime) IsFinished() bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
if m.Status == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return *m.Status == MediaStatusFinished
|
||||
}
|
||||
|
||||
func (m *CompleteAnime) GetAllTitles() []*string {
|
||||
titles := make([]*string, 0)
|
||||
if m.HasRomajiTitle() {
|
||||
titles = append(titles, m.Title.Romaji)
|
||||
}
|
||||
if m.HasEnglishTitle() {
|
||||
titles = append(titles, m.Title.English)
|
||||
}
|
||||
if m.HasSynonyms() && len(m.Synonyms) > 1 {
|
||||
titles = append(titles, lo.Filter(m.Synonyms, func(s *string, i int) bool { return comparison.ValueContainsSeason(*s) })...)
|
||||
}
|
||||
return titles
|
||||
}
|
||||
|
||||
func (m *CompleteAnime) GetAllTitlesDeref() []string {
|
||||
titles := make([]string, 0)
|
||||
if m.HasRomajiTitle() {
|
||||
titles = append(titles, *m.Title.Romaji)
|
||||
}
|
||||
if m.HasEnglishTitle() {
|
||||
titles = append(titles, *m.Title.English)
|
||||
}
|
||||
if m.HasSynonyms() && len(m.Synonyms) > 1 {
|
||||
syn := lo.Filter(m.Synonyms, func(s *string, i int) bool { return comparison.ValueContainsSeason(*s) })
|
||||
for _, s := range syn {
|
||||
titles = append(titles, *s)
|
||||
}
|
||||
}
|
||||
return titles
|
||||
}
|
||||
|
||||
// GetCurrentEpisodeCount returns the current episode number for that media and -1 if it doesn't have one.
|
||||
// i.e. -1 is returned if the media has no episodes AND the next airing episode is not set.
|
||||
func (m *CompleteAnime) GetCurrentEpisodeCount() int {
|
||||
ceil := -1
|
||||
if m.Episodes != nil {
|
||||
ceil = *m.Episodes
|
||||
}
|
||||
if m.NextAiringEpisode != nil {
|
||||
if m.NextAiringEpisode.Episode > 0 {
|
||||
ceil = m.NextAiringEpisode.Episode - 1
|
||||
}
|
||||
}
|
||||
return ceil
|
||||
}
|
||||
|
||||
// GetTotalEpisodeCount returns the total episode number for that media and -1 if it doesn't have one
|
||||
func (m *CompleteAnime) GetTotalEpisodeCount() int {
|
||||
ceil := -1
|
||||
if m.Episodes != nil {
|
||||
ceil = *m.Episodes
|
||||
}
|
||||
return ceil
|
||||
}
|
||||
|
||||
// GetPossibleSeasonNumber returns the possible season number for that media and -1 if it doesn't have one.
|
||||
// It looks at the synonyms and returns the highest season number found.
|
||||
func (m *CompleteAnime) GetPossibleSeasonNumber() int {
|
||||
if m == nil || m.Synonyms == nil || len(m.Synonyms) == 0 {
|
||||
return -1
|
||||
}
|
||||
titles := lo.Filter(m.Synonyms, func(s *string, i int) bool { return comparison.ValueContainsSeason(*s) })
|
||||
if m.HasEnglishTitle() {
|
||||
titles = append(titles, m.Title.English)
|
||||
}
|
||||
if m.HasRomajiTitle() {
|
||||
titles = append(titles, m.Title.Romaji)
|
||||
}
|
||||
seasons := lo.Map(titles, func(s *string, i int) int { return comparison.ExtractSeasonNumber(*s) })
|
||||
return lo.Max(seasons)
|
||||
}
|
||||
|
||||
func (m *CompleteAnime) HasEnglishTitle() bool {
|
||||
return m.Title.English != nil
|
||||
}
|
||||
|
||||
func (m *CompleteAnime) HasRomajiTitle() bool {
|
||||
return m.Title.Romaji != nil
|
||||
}
|
||||
|
||||
func (m *CompleteAnime) HasSynonyms() bool {
|
||||
return m.Synonyms != nil
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
var EdgeNarrowFormats = []MediaFormat{MediaFormatTv, MediaFormatTvShort}
|
||||
var EdgeBroaderFormats = []MediaFormat{MediaFormatTv, MediaFormatTvShort, MediaFormatOna, MediaFormatOva, MediaFormatMovie, MediaFormatSpecial}
|
||||
|
||||
func (m *CompleteAnime) FindEdge(relation string, formats []MediaFormat) (*BaseAnime, bool) {
|
||||
if m.GetRelations() == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
edges := m.GetRelations().GetEdges()
|
||||
|
||||
for _, edge := range edges {
|
||||
|
||||
if edge.GetRelationType().String() == relation {
|
||||
for _, fm := range formats {
|
||||
if fm.String() == edge.GetNode().GetFormat().String() {
|
||||
return edge.GetNode(), true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (e *CompleteAnime_Relations_Edges) IsBroadRelationFormat() bool {
|
||||
if e.GetNode() == nil {
|
||||
return false
|
||||
}
|
||||
if e.GetNode().GetFormat() == nil {
|
||||
return false
|
||||
}
|
||||
for _, fm := range EdgeBroaderFormats {
|
||||
if fm.String() == e.GetNode().GetFormat().String() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
func (e *CompleteAnime_Relations_Edges) IsNarrowRelationFormat() bool {
|
||||
if e.GetNode() == nil {
|
||||
return false
|
||||
}
|
||||
if e.GetNode().GetFormat() == nil {
|
||||
return false
|
||||
}
|
||||
for _, fm := range EdgeNarrowFormats {
|
||||
if fm.String() == e.GetNode().GetFormat().String() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func (m *CompleteAnime) ToBaseAnime() *BaseAnime {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var trailer *BaseAnime_Trailer
|
||||
if m.GetTrailer() != nil {
|
||||
trailer = &BaseAnime_Trailer{
|
||||
ID: m.GetTrailer().GetID(),
|
||||
Site: m.GetTrailer().GetSite(),
|
||||
Thumbnail: m.GetTrailer().GetThumbnail(),
|
||||
}
|
||||
}
|
||||
|
||||
var nextAiringEpisode *BaseAnime_NextAiringEpisode
|
||||
if m.GetNextAiringEpisode() != nil {
|
||||
nextAiringEpisode = &BaseAnime_NextAiringEpisode{
|
||||
AiringAt: m.GetNextAiringEpisode().GetAiringAt(),
|
||||
TimeUntilAiring: m.GetNextAiringEpisode().GetTimeUntilAiring(),
|
||||
Episode: m.GetNextAiringEpisode().GetEpisode(),
|
||||
}
|
||||
}
|
||||
|
||||
var startDate *BaseAnime_StartDate
|
||||
if m.GetStartDate() != nil {
|
||||
startDate = &BaseAnime_StartDate{
|
||||
Year: m.GetStartDate().GetYear(),
|
||||
Month: m.GetStartDate().GetMonth(),
|
||||
Day: m.GetStartDate().GetDay(),
|
||||
}
|
||||
}
|
||||
|
||||
var endDate *BaseAnime_EndDate
|
||||
if m.GetEndDate() != nil {
|
||||
endDate = &BaseAnime_EndDate{
|
||||
Year: m.GetEndDate().GetYear(),
|
||||
Month: m.GetEndDate().GetMonth(),
|
||||
Day: m.GetEndDate().GetDay(),
|
||||
}
|
||||
}
|
||||
|
||||
return &BaseAnime{
|
||||
ID: m.GetID(),
|
||||
IDMal: m.GetIDMal(),
|
||||
SiteURL: m.GetSiteURL(),
|
||||
Format: m.GetFormat(),
|
||||
Episodes: m.GetEpisodes(),
|
||||
Status: m.GetStatus(),
|
||||
Synonyms: m.GetSynonyms(),
|
||||
BannerImage: m.GetBannerImage(),
|
||||
Season: m.GetSeason(),
|
||||
SeasonYear: m.GetSeasonYear(),
|
||||
Type: m.GetType(),
|
||||
IsAdult: m.GetIsAdult(),
|
||||
CountryOfOrigin: m.GetCountryOfOrigin(),
|
||||
Genres: m.GetGenres(),
|
||||
Duration: m.GetDuration(),
|
||||
Description: m.GetDescription(),
|
||||
MeanScore: m.GetMeanScore(),
|
||||
Trailer: trailer,
|
||||
Title: &BaseAnime_Title{
|
||||
UserPreferred: m.GetTitle().GetUserPreferred(),
|
||||
Romaji: m.GetTitle().GetRomaji(),
|
||||
English: m.GetTitle().GetEnglish(),
|
||||
Native: m.GetTitle().GetNative(),
|
||||
},
|
||||
CoverImage: &BaseAnime_CoverImage{
|
||||
ExtraLarge: m.GetCoverImage().GetExtraLarge(),
|
||||
Large: m.GetCoverImage().GetLarge(),
|
||||
Medium: m.GetCoverImage().GetMedium(),
|
||||
Color: m.GetCoverImage().GetColor(),
|
||||
},
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
NextAiringEpisode: nextAiringEpisode,
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func (m *AnimeListEntry) GetProgressSafe() int {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
if m.Progress == nil {
|
||||
return 0
|
||||
}
|
||||
return *m.Progress
|
||||
}
|
||||
|
||||
func (m *AnimeListEntry) GetScoreSafe() float64 {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
if m.Score == nil {
|
||||
return 0
|
||||
}
|
||||
return *m.Score
|
||||
}
|
||||
|
||||
func (m *AnimeListEntry) GetRepeatSafe() int {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
if m.Repeat == nil {
|
||||
return 0
|
||||
}
|
||||
return *m.Repeat
|
||||
}
|
||||
|
||||
func (m *AnimeListEntry) GetStatusSafe() MediaListStatus {
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
if m.Status == nil {
|
||||
return ""
|
||||
}
|
||||
return *m.Status
|
||||
}
|
||||
155
seanime-2.9.10/internal/api/anilist/media_tree.go
Normal file
155
seanime-2.9.10/internal/api/anilist/media_tree.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package anilist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/samber/lo"
|
||||
"seanime/internal/util"
|
||||
"seanime/internal/util/limiter"
|
||||
"seanime/internal/util/result"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type (
|
||||
CompleteAnimeRelationTree struct {
|
||||
*result.Map[int, *CompleteAnime]
|
||||
}
|
||||
|
||||
FetchMediaTreeRelation = string
|
||||
)
|
||||
|
||||
const (
|
||||
FetchMediaTreeSequels FetchMediaTreeRelation = "sequels"
|
||||
FetchMediaTreePrequels FetchMediaTreeRelation = "prequels"
|
||||
FetchMediaTreeAll FetchMediaTreeRelation = "all"
|
||||
)
|
||||
|
||||
// NewCompleteAnimeRelationTree returns a new result.Map[int, *CompleteAnime].
|
||||
// It is used to store the results of FetchMediaTree or FetchMediaTree calls.
|
||||
func NewCompleteAnimeRelationTree() *CompleteAnimeRelationTree {
|
||||
return &CompleteAnimeRelationTree{result.NewResultMap[int, *CompleteAnime]()}
|
||||
}
|
||||
|
||||
func (m *BaseAnime) FetchMediaTree(rel FetchMediaTreeRelation, anilistClient AnilistClient, rl *limiter.Limiter, tree *CompleteAnimeRelationTree, cache *CompleteAnimeCache) (err error) {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer util.HandlePanicInModuleWithError("anilist/BaseAnime.FetchMediaTree", &err)
|
||||
|
||||
rl.Wait()
|
||||
res, err := anilistClient.CompleteAnimeByID(context.Background(), &m.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return res.GetMedia().FetchMediaTree(rel, anilistClient, rl, tree, cache)
|
||||
}
|
||||
|
||||
// FetchMediaTree populates the CompleteAnimeRelationTree with the given media's sequels and prequels.
|
||||
// It also takes a CompleteAnimeCache to store the fetched media in and avoid duplicate fetches.
|
||||
// It also takes a limiter.Limiter to limit the number of requests made to the AniList API.
|
||||
func (m *CompleteAnime) FetchMediaTree(rel FetchMediaTreeRelation, anilistClient AnilistClient, rl *limiter.Limiter, tree *CompleteAnimeRelationTree, cache *CompleteAnimeCache) (err error) {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer util.HandlePanicInModuleWithError("anilist/CompleteAnime.FetchMediaTree", &err)
|
||||
|
||||
if tree.Has(m.ID) {
|
||||
cache.Set(m.ID, m)
|
||||
return nil
|
||||
}
|
||||
cache.Set(m.ID, m)
|
||||
tree.Set(m.ID, m)
|
||||
|
||||
if m.Relations == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get all edges
|
||||
edges := m.GetRelations().GetEdges()
|
||||
// Filter edges
|
||||
edges = lo.Filter(edges, func(_edge *CompleteAnime_Relations_Edges, _ int) bool {
|
||||
return (*_edge.RelationType == MediaRelationSequel || *_edge.RelationType == MediaRelationPrequel) &&
|
||||
*_edge.GetNode().Status != MediaStatusNotYetReleased &&
|
||||
_edge.IsBroadRelationFormat() && !tree.Has(_edge.GetNode().ID)
|
||||
})
|
||||
|
||||
if len(edges) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
doneCh := make(chan struct{})
|
||||
processEdges(edges, rel, anilistClient, rl, tree, cache, doneCh)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-doneCh:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processEdges fetches the next node(s) for each edge in parallel.
|
||||
func processEdges(edges []*CompleteAnime_Relations_Edges, rel FetchMediaTreeRelation, anilistClient AnilistClient, rl *limiter.Limiter, tree *CompleteAnimeRelationTree, cache *CompleteAnimeCache, doneCh chan struct{}) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(edges))
|
||||
|
||||
for i, item := range edges {
|
||||
go func(edge *CompleteAnime_Relations_Edges, _ int) {
|
||||
defer wg.Done()
|
||||
if edge == nil {
|
||||
return
|
||||
}
|
||||
processEdge(edge, rel, anilistClient, rl, tree, cache)
|
||||
}(item, i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
go func() {
|
||||
close(doneCh)
|
||||
}()
|
||||
}
|
||||
|
||||
func processEdge(edge *CompleteAnime_Relations_Edges, rel FetchMediaTreeRelation, anilistClient AnilistClient, rl *limiter.Limiter, tree *CompleteAnimeRelationTree, cache *CompleteAnimeCache) {
|
||||
defer util.HandlePanicInModuleThen("anilist/processEdge", func() {})
|
||||
cacheV, ok := cache.Get(edge.GetNode().ID)
|
||||
edgeCompleteAnime := cacheV
|
||||
if !ok {
|
||||
rl.Wait()
|
||||
// Fetch the next node
|
||||
res, err := anilistClient.CompleteAnimeByID(context.Background(), &edge.GetNode().ID)
|
||||
if err == nil {
|
||||
edgeCompleteAnime = res.GetMedia()
|
||||
cache.Set(edgeCompleteAnime.ID, edgeCompleteAnime)
|
||||
}
|
||||
}
|
||||
if edgeCompleteAnime == nil {
|
||||
return
|
||||
}
|
||||
// Get the relation type to fetch for the next node
|
||||
edgeRel := getEdgeRelation(edge, rel)
|
||||
// Fetch the next node(s)
|
||||
err := edgeCompleteAnime.FetchMediaTree(edgeRel, anilistClient, rl, tree, cache)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// getEdgeRelation returns the relation to fetch for the next node based on the current edge and the relation to fetch.
|
||||
// If the relation to fetch is FetchMediaTreeAll, it will return FetchMediaTreePrequels for prequels and FetchMediaTreeSequels for sequels.
|
||||
//
|
||||
// For example, if the current node is a sequel and the relation to fetch is FetchMediaTreeAll, it will return FetchMediaTreeSequels so that
|
||||
// only sequels are fetched for the next node.
|
||||
func getEdgeRelation(edge *CompleteAnime_Relations_Edges, rel FetchMediaTreeRelation) FetchMediaTreeRelation {
|
||||
if rel == FetchMediaTreeAll {
|
||||
if *edge.RelationType == MediaRelationPrequel {
|
||||
return FetchMediaTreePrequels
|
||||
}
|
||||
if *edge.RelationType == MediaRelationSequel {
|
||||
return FetchMediaTreeSequels
|
||||
}
|
||||
}
|
||||
return rel
|
||||
}
|
||||
82
seanime-2.9.10/internal/api/anilist/media_tree_test.go
Normal file
82
seanime-2.9.10/internal/api/anilist/media_tree_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package anilist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"seanime/internal/test_utils"
|
||||
"seanime/internal/util/limiter"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBaseAnime_FetchMediaTree_BaseAnime(t *testing.T) {
|
||||
test_utils.InitTestProvider(t, test_utils.Anilist())
|
||||
|
||||
anilistClient := TestGetMockAnilistClient()
|
||||
lim := limiter.NewAnilistLimiter()
|
||||
completeAnimeCache := NewCompleteAnimeCache()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mediaId int
|
||||
edgeIds []int
|
||||
}{
|
||||
{
|
||||
name: "Bungo Stray Dogs",
|
||||
mediaId: 103223,
|
||||
edgeIds: []int{
|
||||
21311, // BSD1
|
||||
21679, // BSD2
|
||||
103223, // BSD3
|
||||
141249, // BSD4
|
||||
163263, // BSD5
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Re:Zero",
|
||||
mediaId: 21355,
|
||||
edgeIds: []int{
|
||||
21355, // Re:Zero 1
|
||||
108632, // Re:Zero 2
|
||||
119661, // Re:Zero 2 Part 2
|
||||
163134, // Re:Zero 3
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
mediaF, err := anilistClient.CompleteAnimeByID(context.Background(), &tt.mediaId)
|
||||
|
||||
if assert.NoError(t, err) {
|
||||
|
||||
media := mediaF.GetMedia()
|
||||
|
||||
tree := NewCompleteAnimeRelationTree()
|
||||
|
||||
err = media.FetchMediaTree(
|
||||
FetchMediaTreeAll,
|
||||
anilistClient,
|
||||
lim,
|
||||
tree,
|
||||
completeAnimeCache,
|
||||
)
|
||||
|
||||
if assert.NoError(t, err) {
|
||||
|
||||
for _, treeId := range tt.edgeIds {
|
||||
a, found := tree.Get(treeId)
|
||||
assert.Truef(t, found, "expected tree to contain %d", treeId)
|
||||
spew.Dump(a.GetTitleSafe())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
4440
seanime-2.9.10/internal/api/anilist/models_gen.go
Normal file
4440
seanime-2.9.10/internal/api/anilist/models_gen.go
Normal file
File diff suppressed because it is too large
Load Diff
456
seanime-2.9.10/internal/api/anilist/queries/anime.graphql
Normal file
456
seanime-2.9.10/internal/api/anilist/queries/anime.graphql
Normal file
@@ -0,0 +1,456 @@
|
||||
query AnimeCollection ($userName: String) {
|
||||
MediaListCollection(userName: $userName, forceSingleCompletedList: true, type: ANIME) {
|
||||
lists {
|
||||
status
|
||||
name
|
||||
isCustomList
|
||||
entries {
|
||||
id
|
||||
score(format: POINT_100)
|
||||
progress
|
||||
status
|
||||
notes
|
||||
repeat
|
||||
private
|
||||
startedAt {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
completedAt {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
media {
|
||||
...baseAnime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query AnimeCollectionWithRelations ($userName: String) {
|
||||
MediaListCollection(userName: $userName, forceSingleCompletedList: true, type: ANIME) {
|
||||
lists {
|
||||
status
|
||||
name
|
||||
isCustomList
|
||||
entries {
|
||||
id
|
||||
score(format: POINT_100)
|
||||
progress
|
||||
status
|
||||
notes
|
||||
repeat
|
||||
private
|
||||
startedAt {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
completedAt {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
media {
|
||||
...completeAnime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query BaseAnimeByMalId ($id: Int) {
|
||||
Media(idMal: $id, type: ANIME) {
|
||||
...baseAnime
|
||||
}
|
||||
}
|
||||
|
||||
query BaseAnimeById ($id: Int) {
|
||||
Media(id: $id, type: ANIME) {
|
||||
...baseAnime
|
||||
}
|
||||
}
|
||||
|
||||
query SearchBaseAnimeByIds ($ids: [Int], $page: Int, $perPage: Int, $status: [MediaStatus], $inCollection: Boolean, $sort: [MediaSort], $season: MediaSeason, $year: Int, $genre: String, $format: MediaFormat) {
|
||||
Page(page: $page, perPage: $perPage) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
},
|
||||
media(id_in: $ids, type: ANIME, status_in: $status, onList: $inCollection, sort: $sort, season: $season, seasonYear: $year, genre: $genre, format: $format) {
|
||||
...baseAnime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query CompleteAnimeById ($id: Int) {
|
||||
Media(id: $id, type: ANIME) {
|
||||
...completeAnime
|
||||
}
|
||||
}
|
||||
|
||||
# For view (will be cached)
|
||||
query AnimeDetailsById ($id: Int) {
|
||||
Media(id: $id, type: ANIME) {
|
||||
siteUrl
|
||||
id
|
||||
duration
|
||||
genres
|
||||
averageScore
|
||||
popularity
|
||||
meanScore
|
||||
description
|
||||
trailer {
|
||||
id
|
||||
site
|
||||
thumbnail
|
||||
}
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
studios(isMain: true) {
|
||||
nodes {
|
||||
name
|
||||
id
|
||||
}
|
||||
}
|
||||
characters(sort: [ROLE]) {
|
||||
edges {
|
||||
id
|
||||
role
|
||||
name
|
||||
node {
|
||||
...baseCharacter
|
||||
}
|
||||
}
|
||||
}
|
||||
staff(sort: [RELEVANCE]) {
|
||||
edges {
|
||||
role
|
||||
node {
|
||||
name {
|
||||
full
|
||||
}
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
rankings {
|
||||
context
|
||||
type
|
||||
rank
|
||||
year
|
||||
format
|
||||
allTime
|
||||
season
|
||||
}
|
||||
recommendations(page: 1, perPage: 8, sort: RATING_DESC) {
|
||||
edges {
|
||||
node {
|
||||
mediaRecommendation {
|
||||
id
|
||||
idMal
|
||||
siteUrl
|
||||
status(version: 2)
|
||||
isAdult
|
||||
season
|
||||
type
|
||||
format
|
||||
meanScore
|
||||
description
|
||||
episodes
|
||||
trailer {
|
||||
id
|
||||
site
|
||||
thumbnail
|
||||
}
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
coverImage {
|
||||
extraLarge
|
||||
large
|
||||
medium
|
||||
color
|
||||
}
|
||||
bannerImage
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
native
|
||||
userPreferred
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
relations {
|
||||
edges {
|
||||
relationType(version: 2)
|
||||
node {
|
||||
...baseAnime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query ListAnime(
|
||||
$page: Int
|
||||
$search: String
|
||||
$perPage: Int
|
||||
$sort: [MediaSort]
|
||||
$status: [MediaStatus]
|
||||
$genres: [String]
|
||||
$averageScore_greater: Int
|
||||
$season: MediaSeason
|
||||
$seasonYear: Int
|
||||
$format: MediaFormat
|
||||
$isAdult: Boolean
|
||||
) {
|
||||
Page(page: $page, perPage: $perPage) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
total
|
||||
perPage
|
||||
currentPage
|
||||
lastPage
|
||||
}
|
||||
media(
|
||||
type: ANIME
|
||||
search: $search
|
||||
sort: $sort
|
||||
status_in: $status
|
||||
isAdult: $isAdult
|
||||
format: $format
|
||||
genre_in: $genres
|
||||
averageScore_greater: $averageScore_greater
|
||||
season: $season
|
||||
seasonYear: $seasonYear
|
||||
format_not: MUSIC
|
||||
) {
|
||||
...baseAnime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query ListRecentAnime ($page: Int, $perPage: Int, $airingAt_greater: Int, $airingAt_lesser: Int, $notYetAired: Boolean = false) {
|
||||
Page(page: $page, perPage: $perPage) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
total
|
||||
perPage
|
||||
currentPage
|
||||
lastPage
|
||||
}
|
||||
airingSchedules(notYetAired: $notYetAired, sort: TIME_DESC, airingAt_greater: $airingAt_greater, airingAt_lesser: $airingAt_lesser) {
|
||||
id
|
||||
airingAt
|
||||
episode
|
||||
timeUntilAiring
|
||||
media {
|
||||
... baseAnime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment baseAnime on Media {
|
||||
id
|
||||
idMal
|
||||
siteUrl
|
||||
status(version: 2)
|
||||
season
|
||||
type
|
||||
format
|
||||
seasonYear
|
||||
bannerImage
|
||||
episodes
|
||||
synonyms
|
||||
isAdult
|
||||
countryOfOrigin
|
||||
meanScore
|
||||
description
|
||||
genres
|
||||
duration
|
||||
trailer {
|
||||
id
|
||||
site
|
||||
thumbnail
|
||||
}
|
||||
title {
|
||||
userPreferred
|
||||
romaji
|
||||
english
|
||||
native
|
||||
}
|
||||
coverImage {
|
||||
extraLarge
|
||||
large
|
||||
medium
|
||||
color
|
||||
}
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
nextAiringEpisode {
|
||||
airingAt
|
||||
timeUntilAiring
|
||||
episode
|
||||
}
|
||||
}
|
||||
|
||||
fragment completeAnime on Media {
|
||||
id
|
||||
idMal
|
||||
siteUrl
|
||||
status(version: 2)
|
||||
season
|
||||
seasonYear
|
||||
type
|
||||
format
|
||||
bannerImage
|
||||
episodes
|
||||
synonyms
|
||||
isAdult
|
||||
countryOfOrigin
|
||||
meanScore
|
||||
description
|
||||
genres
|
||||
duration
|
||||
trailer {
|
||||
id
|
||||
site
|
||||
thumbnail
|
||||
}
|
||||
title {
|
||||
userPreferred
|
||||
romaji
|
||||
english
|
||||
native
|
||||
}
|
||||
coverImage {
|
||||
extraLarge
|
||||
large
|
||||
medium
|
||||
color
|
||||
}
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
nextAiringEpisode {
|
||||
airingAt
|
||||
timeUntilAiring
|
||||
episode
|
||||
}
|
||||
relations {
|
||||
edges {
|
||||
relationType(version: 2)
|
||||
node {
|
||||
...baseAnime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment baseCharacter on Character {
|
||||
id
|
||||
isFavourite
|
||||
gender
|
||||
age
|
||||
dateOfBirth {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
name {
|
||||
full
|
||||
native
|
||||
alternative
|
||||
}
|
||||
image {
|
||||
large
|
||||
}
|
||||
description
|
||||
siteUrl
|
||||
}
|
||||
|
||||
query AnimeAiringSchedule($ids: [Int],$season: MediaSeason, $seasonYear: Int, $previousSeason: MediaSeason, $previousSeasonYear: Int, $nextSeason: MediaSeason, $nextSeasonYear: Int) {
|
||||
ongoing: Page {
|
||||
media(id_in: $ids, type: ANIME, season: $season, seasonYear: $seasonYear, onList: true) {
|
||||
...animeSchedule
|
||||
}
|
||||
}
|
||||
ongoingNext: Page(page: 2) {
|
||||
media(id_in: $ids, type: ANIME, season: $season, seasonYear: $seasonYear, onList: true) {
|
||||
...animeSchedule
|
||||
}
|
||||
}
|
||||
upcoming: Page {
|
||||
media(id_in: $ids, type: ANIME, season: $nextSeason, seasonYear: $nextSeasonYear, sort: [START_DATE], onList: true) {
|
||||
...animeSchedule
|
||||
}
|
||||
}
|
||||
upcomingNext: Page(page: 2) {
|
||||
media(id_in: $ids, type: ANIME, season: $nextSeason, seasonYear: $nextSeasonYear, sort: [START_DATE], onList: true) {
|
||||
...animeSchedule
|
||||
}
|
||||
}
|
||||
preceding: Page {
|
||||
media(id_in: $ids, type: ANIME, season: $previousSeason, seasonYear: $previousSeasonYear, onList: true) {
|
||||
...animeSchedule
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query AnimeAiringScheduleRaw($ids: [Int]) {
|
||||
Page {
|
||||
media(id_in: $ids, type: ANIME, onList: true) {
|
||||
...animeSchedule
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment animeSchedule on Media {
|
||||
id,
|
||||
idMal
|
||||
previous: airingSchedule(notYetAired: false, perPage: 30) {
|
||||
nodes {
|
||||
airingAt
|
||||
timeUntilAiring
|
||||
episode
|
||||
}
|
||||
},
|
||||
upcoming: airingSchedule(notYetAired: true, perPage: 30) {
|
||||
nodes {
|
||||
airingAt
|
||||
timeUntilAiring
|
||||
episode
|
||||
}
|
||||
}
|
||||
}
|
||||
56
seanime-2.9.10/internal/api/anilist/queries/entry.graphql
Normal file
56
seanime-2.9.10/internal/api/anilist/queries/entry.graphql
Normal file
@@ -0,0 +1,56 @@
|
||||
mutation UpdateMediaListEntry (
|
||||
$mediaId: Int
|
||||
$status: MediaListStatus
|
||||
$scoreRaw: Int
|
||||
$progress: Int
|
||||
$startedAt: FuzzyDateInput
|
||||
$completedAt: FuzzyDateInput
|
||||
) {
|
||||
SaveMediaListEntry(
|
||||
mediaId: $mediaId
|
||||
status: $status
|
||||
scoreRaw: $scoreRaw
|
||||
progress: $progress
|
||||
startedAt: $startedAt
|
||||
completedAt: $completedAt
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
mutation UpdateMediaListEntryProgress (
|
||||
$mediaId: Int
|
||||
$progress: Int
|
||||
$status: MediaListStatus
|
||||
) {
|
||||
SaveMediaListEntry(
|
||||
mediaId: $mediaId
|
||||
progress: $progress
|
||||
status: $status
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
mutation DeleteEntry (
|
||||
$mediaListEntryId: Int
|
||||
) {
|
||||
DeleteMediaListEntry(
|
||||
id: $mediaListEntryId
|
||||
) {
|
||||
deleted
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mutation UpdateMediaListEntryRepeat (
|
||||
$mediaId: Int
|
||||
$repeat: Int
|
||||
) {
|
||||
SaveMediaListEntry(
|
||||
mediaId: $mediaId
|
||||
repeat: $repeat
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
200
seanime-2.9.10/internal/api/anilist/queries/manga.graphql
Normal file
200
seanime-2.9.10/internal/api/anilist/queries/manga.graphql
Normal file
@@ -0,0 +1,200 @@
|
||||
query MangaCollection ($userName: String) {
|
||||
MediaListCollection(userName: $userName, forceSingleCompletedList: true, type: MANGA) {
|
||||
lists {
|
||||
status
|
||||
name
|
||||
isCustomList
|
||||
entries {
|
||||
id
|
||||
score(format: POINT_100)
|
||||
progress
|
||||
status
|
||||
notes
|
||||
repeat
|
||||
private
|
||||
startedAt {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
completedAt {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
media {
|
||||
...baseManga
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
query SearchBaseManga($page: Int, $perPage: Int, $sort: [MediaSort], $search: String, $status: [MediaStatus]){
|
||||
Page(page: $page, perPage: $perPage){
|
||||
pageInfo{
|
||||
hasNextPage
|
||||
},
|
||||
media(type: MANGA, search: $search, sort: $sort, status_in: $status, format_not: NOVEL){
|
||||
...baseManga
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query BaseMangaById ($id: Int) {
|
||||
Media(id: $id, type: MANGA) {
|
||||
...baseManga
|
||||
}
|
||||
}
|
||||
|
||||
# For view (will be cached)
|
||||
query MangaDetailsById ($id: Int) {
|
||||
Media(id: $id, type: MANGA) {
|
||||
siteUrl
|
||||
id
|
||||
duration
|
||||
genres
|
||||
rankings {
|
||||
context
|
||||
type
|
||||
rank
|
||||
year
|
||||
format
|
||||
allTime
|
||||
season
|
||||
}
|
||||
characters(sort: [ROLE]) {
|
||||
edges {
|
||||
id
|
||||
role
|
||||
name
|
||||
node {
|
||||
...baseCharacter
|
||||
}
|
||||
}
|
||||
}
|
||||
recommendations(page: 1, perPage: 8, sort: RATING_DESC) {
|
||||
edges {
|
||||
node {
|
||||
mediaRecommendation {
|
||||
id
|
||||
idMal
|
||||
siteUrl
|
||||
status(version: 2)
|
||||
season
|
||||
type
|
||||
format
|
||||
bannerImage
|
||||
chapters
|
||||
volumes
|
||||
synonyms
|
||||
isAdult
|
||||
countryOfOrigin
|
||||
meanScore
|
||||
description
|
||||
title {
|
||||
userPreferred
|
||||
romaji
|
||||
english
|
||||
native
|
||||
}
|
||||
coverImage {
|
||||
extraLarge
|
||||
large
|
||||
medium
|
||||
color
|
||||
}
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
relations {
|
||||
edges {
|
||||
relationType(version: 2)
|
||||
node {
|
||||
...baseManga
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query ListManga(
|
||||
$page: Int
|
||||
$search: String
|
||||
$perPage: Int
|
||||
$sort: [MediaSort]
|
||||
$status: [MediaStatus]
|
||||
$genres: [String]
|
||||
$averageScore_greater: Int
|
||||
$startDate_greater: FuzzyDateInt
|
||||
$startDate_lesser: FuzzyDateInt
|
||||
$format: MediaFormat
|
||||
$countryOfOrigin: CountryCode
|
||||
$isAdult: Boolean
|
||||
) {
|
||||
Page(page: $page, perPage: $perPage){
|
||||
pageInfo{
|
||||
hasNextPage
|
||||
total
|
||||
perPage
|
||||
currentPage
|
||||
lastPage
|
||||
},
|
||||
media(type: MANGA, isAdult: $isAdult, countryOfOrigin: $countryOfOrigin, search: $search, sort: $sort, status_in: $status, format: $format, genre_in: $genres, averageScore_greater: $averageScore_greater, startDate_greater: $startDate_greater, startDate_lesser: $startDate_lesser, format_not: NOVEL){
|
||||
...baseManga
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment baseManga on Media {
|
||||
id
|
||||
idMal
|
||||
siteUrl
|
||||
status(version: 2)
|
||||
season
|
||||
type
|
||||
format
|
||||
bannerImage
|
||||
chapters
|
||||
volumes
|
||||
synonyms
|
||||
isAdult
|
||||
countryOfOrigin
|
||||
meanScore
|
||||
description
|
||||
genres
|
||||
title {
|
||||
userPreferred
|
||||
romaji
|
||||
english
|
||||
native
|
||||
}
|
||||
coverImage {
|
||||
extraLarge
|
||||
large
|
||||
medium
|
||||
color
|
||||
}
|
||||
startDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
endDate {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
}
|
||||
126
seanime-2.9.10/internal/api/anilist/queries/stats.graphql
Normal file
126
seanime-2.9.10/internal/api/anilist/queries/stats.graphql
Normal file
@@ -0,0 +1,126 @@
|
||||
query ViewerStats {
|
||||
Viewer {
|
||||
statistics {
|
||||
anime {
|
||||
count
|
||||
minutesWatched
|
||||
episodesWatched
|
||||
meanScore
|
||||
formats {
|
||||
...UserFormatStats
|
||||
}
|
||||
genres {
|
||||
...UserGenreStats
|
||||
}
|
||||
statuses {
|
||||
...UserStatusStats
|
||||
}
|
||||
studios {
|
||||
...UserStudioStats
|
||||
}
|
||||
scores {
|
||||
...UserScoreStats
|
||||
}
|
||||
startYears {
|
||||
...UserStartYearStats
|
||||
}
|
||||
releaseYears {
|
||||
...UserReleaseYearStats
|
||||
}
|
||||
}
|
||||
manga {
|
||||
count
|
||||
chaptersRead
|
||||
meanScore
|
||||
formats {
|
||||
...UserFormatStats
|
||||
}
|
||||
genres {
|
||||
...UserGenreStats
|
||||
}
|
||||
statuses {
|
||||
...UserStatusStats
|
||||
}
|
||||
studios {
|
||||
...UserStudioStats
|
||||
}
|
||||
scores {
|
||||
...UserScoreStats
|
||||
}
|
||||
startYears {
|
||||
...UserStartYearStats
|
||||
}
|
||||
releaseYears {
|
||||
...UserReleaseYearStats
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment UserFormatStats on UserFormatStatistic {
|
||||
format
|
||||
meanScore
|
||||
count
|
||||
minutesWatched
|
||||
mediaIds
|
||||
chaptersRead
|
||||
}
|
||||
|
||||
fragment UserGenreStats on UserGenreStatistic {
|
||||
genre
|
||||
meanScore
|
||||
count
|
||||
minutesWatched
|
||||
mediaIds
|
||||
chaptersRead
|
||||
}
|
||||
|
||||
fragment UserStatusStats on UserStatusStatistic {
|
||||
status
|
||||
meanScore
|
||||
count
|
||||
minutesWatched
|
||||
mediaIds
|
||||
chaptersRead
|
||||
}
|
||||
|
||||
fragment UserScoreStats on UserScoreStatistic {
|
||||
score
|
||||
meanScore
|
||||
count
|
||||
minutesWatched
|
||||
mediaIds
|
||||
chaptersRead
|
||||
}
|
||||
|
||||
fragment UserStudioStats on UserStudioStatistic {
|
||||
studio {
|
||||
id
|
||||
name
|
||||
isAnimationStudio
|
||||
}
|
||||
meanScore
|
||||
count
|
||||
minutesWatched
|
||||
mediaIds
|
||||
chaptersRead
|
||||
}
|
||||
|
||||
fragment UserStartYearStats on UserStartYearStatistic {
|
||||
startYear
|
||||
meanScore
|
||||
count
|
||||
minutesWatched
|
||||
mediaIds
|
||||
chaptersRead
|
||||
}
|
||||
|
||||
fragment UserReleaseYearStats on UserReleaseYearStatistic {
|
||||
releaseYear
|
||||
meanScore
|
||||
count
|
||||
minutesWatched
|
||||
mediaIds
|
||||
chaptersRead
|
||||
}
|
||||
12
seanime-2.9.10/internal/api/anilist/queries/studio.graphql
Normal file
12
seanime-2.9.10/internal/api/anilist/queries/studio.graphql
Normal file
@@ -0,0 +1,12 @@
|
||||
query StudioDetails($id: Int) {
|
||||
Studio(id: $id) {
|
||||
id
|
||||
isAnimationStudio
|
||||
name
|
||||
media (perPage: 80, sort: TRENDING_DESC, isMain: true) {
|
||||
nodes {
|
||||
...baseAnime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
seanime-2.9.10/internal/api/anilist/queries/viewer.graphql
Normal file
16
seanime-2.9.10/internal/api/anilist/queries/viewer.graphql
Normal file
@@ -0,0 +1,16 @@
|
||||
query GetViewer {
|
||||
Viewer {
|
||||
name
|
||||
avatar {
|
||||
large
|
||||
medium
|
||||
}
|
||||
bannerImage
|
||||
isBlocked
|
||||
options {
|
||||
displayAdultContent
|
||||
airingNotifications
|
||||
profileColor
|
||||
}
|
||||
}
|
||||
}
|
||||
72
seanime-2.9.10/internal/api/anilist/stats.go
Normal file
72
seanime-2.9.10/internal/api/anilist/stats.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package anilist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"seanime/internal/util"
|
||||
)
|
||||
|
||||
type (
|
||||
Stats struct {
|
||||
AnimeStats *AnimeStats `json:"animeStats"`
|
||||
MangaStats *MangaStats `json:"mangaStats"`
|
||||
}
|
||||
|
||||
AnimeStats struct {
|
||||
Count int `json:"count"`
|
||||
MinutesWatched int `json:"minutesWatched"`
|
||||
EpisodesWatched int `json:"episodesWatched"`
|
||||
MeanScore float64 `json:"meanScore"`
|
||||
Genres []*UserGenreStats `json:"genres"`
|
||||
Formats []*UserFormatStats `json:"formats"`
|
||||
Statuses []*UserStatusStats `json:"statuses"`
|
||||
Studios []*UserStudioStats `json:"studios"`
|
||||
Scores []*UserScoreStats `json:"scores"`
|
||||
StartYears []*UserStartYearStats `json:"startYears"`
|
||||
ReleaseYears []*UserReleaseYearStats `json:"releaseYears"`
|
||||
}
|
||||
|
||||
MangaStats struct {
|
||||
Count int `json:"count"`
|
||||
ChaptersRead int `json:"chaptersRead"`
|
||||
MeanScore float64 `json:"meanScore"`
|
||||
Genres []*UserGenreStats `json:"genres"`
|
||||
Statuses []*UserStatusStats `json:"statuses"`
|
||||
Scores []*UserScoreStats `json:"scores"`
|
||||
StartYears []*UserStartYearStats `json:"startYears"`
|
||||
ReleaseYears []*UserReleaseYearStats `json:"releaseYears"`
|
||||
}
|
||||
)
|
||||
|
||||
func GetStats(ctx context.Context, stats *ViewerStats) (ret *Stats, err error) {
|
||||
defer util.HandlePanicInModuleWithError("api/anilist/GetStats", &err)
|
||||
|
||||
allStats := stats.GetViewer().GetStatistics()
|
||||
|
||||
ret = &Stats{
|
||||
AnimeStats: &AnimeStats{
|
||||
Count: allStats.GetAnime().GetCount(),
|
||||
MinutesWatched: allStats.GetAnime().GetMinutesWatched(),
|
||||
EpisodesWatched: allStats.GetAnime().GetEpisodesWatched(),
|
||||
MeanScore: allStats.GetAnime().GetMeanScore(),
|
||||
Genres: allStats.GetAnime().GetGenres(),
|
||||
Formats: allStats.GetAnime().GetFormats(),
|
||||
Statuses: allStats.GetAnime().GetStatuses(),
|
||||
Studios: allStats.GetAnime().GetStudios(),
|
||||
Scores: allStats.GetAnime().GetScores(),
|
||||
StartYears: allStats.GetAnime().GetStartYears(),
|
||||
ReleaseYears: allStats.GetAnime().GetReleaseYears(),
|
||||
},
|
||||
MangaStats: &MangaStats{
|
||||
Count: allStats.GetManga().GetCount(),
|
||||
ChaptersRead: allStats.GetManga().GetChaptersRead(),
|
||||
MeanScore: allStats.GetManga().GetMeanScore(),
|
||||
Genres: allStats.GetManga().GetGenres(),
|
||||
Statuses: allStats.GetManga().GetStatuses(),
|
||||
Scores: allStats.GetManga().GetScores(),
|
||||
StartYears: allStats.GetManga().GetStartYears(),
|
||||
ReleaseYears: allStats.GetManga().GetReleaseYears(),
|
||||
},
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
60
seanime-2.9.10/internal/api/anilist/utils.go
Normal file
60
seanime-2.9.10/internal/api/anilist/utils.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package anilist
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type GetSeasonKind int
|
||||
|
||||
const (
|
||||
GetSeasonKindCurrent GetSeasonKind = iota
|
||||
GetSeasonKindNext
|
||||
GetSeasonKindPrevious
|
||||
)
|
||||
|
||||
func GetSeasonInfo(now time.Time, kind GetSeasonKind) (MediaSeason, int) {
|
||||
month, year := now.Month(), now.Year()
|
||||
|
||||
getSeasonIndex := func(m time.Month) int {
|
||||
switch {
|
||||
case m >= 3 && m <= 5: // spring: 3, 4, 5
|
||||
return 1
|
||||
case m >= 6 && m <= 8: // summer: 6, 7, 8
|
||||
return 2
|
||||
case m >= 9 && m <= 11: // fall: 9, 10, 11
|
||||
return 3
|
||||
default: // winter: 12, 1, 2
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
seasons := []MediaSeason{MediaSeasonWinter, MediaSeasonSpring, MediaSeasonSummer, MediaSeasonFall}
|
||||
var index int
|
||||
|
||||
switch kind {
|
||||
case GetSeasonKindCurrent:
|
||||
index = getSeasonIndex(month)
|
||||
|
||||
case GetSeasonKindNext:
|
||||
nextMonth := month + 3
|
||||
nextYear := year
|
||||
if nextMonth > 12 {
|
||||
nextMonth -= 12
|
||||
nextYear++
|
||||
}
|
||||
index = getSeasonIndex(nextMonth)
|
||||
year = nextYear
|
||||
|
||||
case GetSeasonKindPrevious:
|
||||
prevMonth := month - 3
|
||||
prevYear := year
|
||||
if prevMonth <= 0 {
|
||||
prevMonth += 12
|
||||
prevYear--
|
||||
}
|
||||
index = getSeasonIndex(prevMonth)
|
||||
year = prevYear
|
||||
}
|
||||
|
||||
return seasons[index], year
|
||||
}
|
||||
34
seanime-2.9.10/internal/api/anilist/utils_test.go
Normal file
34
seanime-2.9.10/internal/api/anilist/utils_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package anilist
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetSeason(t *testing.T) {
|
||||
tests := []struct {
|
||||
now time.Time
|
||||
kind GetSeasonKind
|
||||
expectedSeason MediaSeason
|
||||
expectedYear int
|
||||
}{
|
||||
{time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), GetSeasonKindCurrent, MediaSeasonWinter, 2025},
|
||||
{time.Date(2025, 4, 1, 0, 0, 0, 0, time.UTC), GetSeasonKindCurrent, MediaSeasonSpring, 2025},
|
||||
{time.Date(2025, 7, 1, 0, 0, 0, 0, time.UTC), GetSeasonKindCurrent, MediaSeasonSummer, 2025},
|
||||
{time.Date(2025, 10, 1, 0, 0, 0, 0, time.UTC), GetSeasonKindCurrent, MediaSeasonFall, 2025},
|
||||
{time.Date(2025, 10, 1, 0, 0, 0, 0, time.UTC), GetSeasonKindNext, MediaSeasonWinter, 2026},
|
||||
{time.Date(2025, 12, 31, 23, 59, 59, 999999999, time.UTC), GetSeasonKindCurrent, MediaSeasonWinter, 2025},
|
||||
{time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), GetSeasonKindNext, MediaSeasonSpring, 2025},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.now.Format(time.RFC3339), func(t *testing.T) {
|
||||
t.Logf("%s", tt.now.Format(time.RFC3339))
|
||||
season, year := GetSeasonInfo(tt.now, tt.kind)
|
||||
require.Equal(t, tt.expectedSeason, season, "Expected season %v, got %v", tt.expectedSeason, season)
|
||||
require.Equal(t, tt.expectedYear, year, "Expected year %d, got %d", tt.expectedYear, year)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user