diff --git a/integrations/playback/allanime/search.go b/integrations/playback/allanime/search.go new file mode 100644 index 0000000..17b88bc --- /dev/null +++ b/integrations/playback/allanime/search.go @@ -0,0 +1,156 @@ +package allanime + +import ( + "context" + "fmt" + "mal/pkg" + netutil "mal/pkg/net" + "strconv" + "strings" +) + +const searchQuery = `query( + $search: SearchInput + $translationType: VaildTranslationTypeEnumType + $limit: Int = 40 + $page: Int = 1 + $countryOrigin: VaildCountryOriginEnumType = ALL +) { + shows( + search: $search + limit: $limit + page: $page + translationType: $translationType + countryOrigin: $countryOrigin + ) { + edges { + _id + malId + name + } + } +}` + +type searchResult struct { + ID string + MalID string + Name string +} + +func (c *AllAnimeProvider) Search(ctx context.Context, query string, mode string) ([]searchResult, error) { + type searchData struct { + Shows struct { + Edges []struct { + ID string `json:"_id"` + MalID string `json:"malId"` + Name string `json:"name"` + } `json:"edges"` + } `json:"shows"` + } + + type searchInput struct { + AllowAdult bool `json:"allowAdult"` + AllowUnknown bool `json:"allowUnknown"` + Query string `json:"query"` + } + + type searchVariables struct { + Search searchInput `json:"search"` + TranslationType string `json:"translationType"` + } + + vars := searchVariables{ + Search: searchInput{ + AllowAdult: false, + AllowUnknown: false, + Query: query, + }, + TranslationType: mode, + } + + data, err := graphql.Post[searchData](ctx, c.httpClient, allAnimeBaseURL+"/api", searchQuery, vars, graphql.PostOptions{ + Headers: map[string]string{ + "Referer": allAnimeReferer, + "User-Agent": defaultUserAgent, + }, + BodyMax: netutil.MiB2, + }) + if err != nil { + return nil, err + } + + out := make([]searchResult, 0, len(data.Shows.Edges)) + for _, edge := range data.Shows.Edges { + id := edge.ID + malID := edge.MalID + name := edge.Name + if unquoted, err := strconv.Unquote("\"" + name + "\""); err == nil { + name = unquoted + } + name = strings.TrimSpace(name) + + if id == "" { + continue + } + + out = append(out, searchResult{ID: id, MalID: malID, Name: name}) + } + + return out, nil +} + +func (c *AllAnimeProvider) resolveShowIDWithFallback(ctx context.Context, animeID int, titleCandidates []string, mode string) string { + targetMalIDStr := strconv.Itoa(animeID) + firstAvailableShowID := "" + + for _, title := range titleCandidates { + searchResults, err := c.Search(ctx, title, mode) + if err != nil || len(searchResults) == 0 { + continue + } + if showID := exactMatchShowID(searchResults, targetMalIDStr); showID != "" { + return showID + } + if firstAvailableShowID == "" { + firstAvailableShowID = searchResults[0].ID + } + } + + return firstAvailableShowID +} + +func exactMatchShowID(searchResults []searchResult, targetMalID string) string { + for _, res := range searchResults { + if res.MalID == targetMalID { + return res.ID + } + } + + return "" +} + +func (c *AllAnimeProvider) ResolveEpisodeProviderID(ctx context.Context, animeID int, titleCandidates []string) (string, error) { + for _, mode := range []string{"sub", "dub"} { + showID, err := c.resolveShowIDStrict(ctx, animeID, titleCandidates, mode) + if err == nil { + return showID, nil + } + } + return "", fmt.Errorf("allanime: no exact mal id match for %d", animeID) +} + +func (c *AllAnimeProvider) resolveShowIDStrict(ctx context.Context, animeID int, titleCandidates []string, mode string) (string, error) { + targetMalIDStr := strconv.Itoa(animeID) + for _, title := range titleCandidates { + searchResults, err := c.Search(ctx, title, mode) + if err != nil { + continue + } + for _, res := range searchResults { + if res.MalID == targetMalIDStr { + return res.ID, nil + } + } + } + return "", fmt.Errorf("allanime: no exact mal id match for %d in %s search", animeID, mode) +}