diff options
Diffstat (limited to 'utils')
| -rw-r--r-- | utils/api/anilist/anilist.go | 219 | ||||
| -rw-r--r-- | utils/api/anilist/types.go | 213 | ||||
| -rw-r--r-- | utils/api/jikan/jikan.go | 388 |
3 files changed, 140 insertions, 680 deletions
diff --git a/utils/api/anilist/anilist.go b/utils/api/anilist/anilist.go index 61e3fbb..7d8036b 100644 --- a/utils/api/anilist/anilist.go +++ b/utils/api/anilist/anilist.go @@ -2,29 +2,131 @@ package anilist import ( "bytes" + "context" "encoding/json" + "errors" "fmt" + "io" + "math" + "metachan/types" "metachan/utils/logger" "net/http" + "strconv" + "time" ) -// AniListClient provides methods for interacting with the AniList API -type AniListClient struct { - client *http.Client - maxRetries int -} +const ( + anilistAPIBaseURL = "https://graphql.anilist.co" + contextTimeout = 60 * time.Second +) -// NewAniListClient creates a new AniList API client -func NewAniListClient() *AniListClient { - return &AniListClient{ - client: &http.Client{}, +var ( + clientInstance = &client{ + httpClient: &http.Client{ + Timeout: 15 * time.Second, + }, maxRetries: 3, + backoff: 1 * time.Second, + } +) + +func (c *client) getBackOffDuration(attempt int) time.Duration { + return time.Duration(float64(c.backoff) * math.Pow(2, float64(attempt-1))) +} + +func (c *client) getRetryAfterDuration(resp *http.Response) time.Duration { + if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" { + if seconds, err := strconv.Atoi(retryAfter); err == nil { + return time.Duration(seconds) * time.Second + } } + return c.backoff } -// GetAnime fetches anime details from AniList by ID using a simpler approach -func (c *AniListClient) GetAnime(anilistID int) (*AnilistAnimeResponse, error) { - // Create a much simpler request with minimal formatting that might trigger Cloudflare +func (c *client) handleRetry(retries *int, reason string, retryAfter time.Duration) bool { + *retries++ + if *retries >= c.maxRetries { + return false + } + + backoffDuration := c.getBackOffDuration(*retries) + if retryAfter > backoffDuration { + backoffDuration = retryAfter + } + + logger.Warnf("AnilistClient", "%s (attempt %d/%d)", reason, *retries, c.maxRetries) + time.Sleep(backoffDuration) + return true +} + +func (c *client) makeRequest(ctx context.Context, query string, variables map[string]interface{}) ([]byte, error) { + var response *http.Response + var retries int + + requestBody := map[string]interface{}{ + "query": query, + "variables": variables, + } + + jsonData, err := json.Marshal(requestBody) + if err != nil { + logger.Errorf("AnilistClient", "Failed to marshal request body: %v", err) + return nil, errors.New("failed to create request to Anilist API") + } + + for retries < c.maxRetries { + request, err := http.NewRequestWithContext(ctx, "POST", anilistAPIBaseURL, bytes.NewBuffer(jsonData)) + if err != nil { + logger.Errorf("AnilistClient", "Failed to create request: %v", err) + return nil, errors.New("failed to create request to Anilist API") + } + + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Accept", "application/json") + request.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") + + response, err = c.httpClient.Do(request) + if err != nil { + if !c.handleRetry(&retries, fmt.Sprintf("Request failed: %v", err), 0) { + logger.Errorf("AnilistClient", "All retries exhausted for request: %v", err) + return nil, errors.New("failed to make request to Anilist API after max retries") + } + continue + } + + defer response.Body.Close() + + switch response.StatusCode { + case http.StatusTooManyRequests: + retryAfter := c.getRetryAfterDuration(response) + if !c.handleRetry(&retries, "Rate limited", retryAfter) { + logger.Errorf("AnilistClient", "All retries exhausted for request") + return nil, errors.New("failed to make request to Anilist API after max retries") + } + case http.StatusOK: + bytes, err := io.ReadAll(response.Body) + + if err != nil { + logger.Errorf("AnilistClient", "Failed to read response body: %v", err) + return nil, errors.New("failed to read response from Anilist API") + } + + return bytes, nil + default: + retries++ + backoffDuration := c.getBackOffDuration(retries) + + logger.Warnf("AnilistClient", "Request returned status %d (attempt %d/%d)", response.StatusCode, retries, c.maxRetries) + + time.Sleep(backoffDuration) + } + } + + logger.Errorf("AnilistClient", "All retries exhausted for request") + return nil, errors.New("failed to make request to Anilist API after max retries") +} + +func GetAnimeByAnilistID(id int) (*types.AnilistAnimeResponse, error) { query := ` query($id: Int) { Media(id: $id, type: ANIME) { @@ -187,88 +289,29 @@ func (c *AniListClient) GetAnime(anilistID int) (*AnilistAnimeResponse, error) { } ` - // Create a simple JSON structure with variables - requestBody := map[string]interface{}{ - "query": query, - "variables": map[string]interface{}{ - "id": anilistID, - }, + variables := map[string]interface{}{ + "id": id, } - jsonData, err := json.Marshal(requestBody) + ctx, cancel := context.WithTimeout(context.Background(), contextTimeout) + defer cancel() + + bytes, err := clientInstance.makeRequest(ctx, query, variables) if err != nil { - return nil, fmt.Errorf("failed to marshal request body: %w", err) + logger.Errorf("AnilistClient", "GetAnime failed for ID %d: %v", id, err) + return nil, errors.New("failed to fetch anime data from Anilist API") } - // Log the request for debugging - logger.Log(fmt.Sprintf("Sending request to AniList for ID %d", anilistID), logger.LogOptions{ - Level: logger.Debug, - Prefix: "AniList", - }) - - var resp *http.Response - var lastErr error - success := false - - for i := 0; i <= c.maxRetries && !success; i++ { - req, err := http.NewRequest("POST", "https://graphql.anilist.co", bytes.NewBuffer(jsonData)) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - - // Add User-Agent to make the request look more like a browser - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") - - resp, err = c.client.Do(req) - if err != nil { - lastErr = err - logger.Log(fmt.Sprintf("AniList request attempt %d failed: %v", i+1, err), logger.LogOptions{ - Level: logger.Debug, - Prefix: "AniList", - }) - continue - } - - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body := make([]byte, 1024) - n, _ := resp.Body.Read(body) - lastErr = fmt.Errorf("server returned %d: %s", resp.StatusCode, string(body[:n])) - logger.Log(fmt.Sprintf("AniList returned non-200 status on attempt %d: %v", i+1, lastErr), logger.LogOptions{ - Level: logger.Debug, - Prefix: "AniList", - }) - continue - } - - var anilistResponse AnilistAnimeResponse - if err := json.NewDecoder(resp.Body).Decode(&anilistResponse); err != nil { - lastErr = fmt.Errorf("failed to decode response: %w", err) - continue - } - - if anilistResponse.Data.Media.ID == 0 { - lastErr = fmt.Errorf("no data found for Anilist ID %d", anilistID) - continue - } - - // Log cover image data for debugging - if anilistResponse.Data.Media.CoverImage.ExtraLarge != "" { - logger.Log(fmt.Sprintf("Found cover data - Color: %s, Image: %s", - anilistResponse.Data.Media.CoverImage.Color, - anilistResponse.Data.Media.CoverImage.ExtraLarge), logger.LogOptions{ - Level: logger.Debug, - Prefix: "AniList", - }) - } + var response types.AnilistAnimeResponse + if err := json.Unmarshal(bytes, &response); err != nil { + logger.Errorf("AnilistClient", "Failed to unmarshal response for ID %d: %v", id, err) + return nil, errors.New("failed to parse anime data from Anilist API") + } - success = true - return &anilistResponse, nil + if response.Data.Media.ID == 0 { + logger.Errorf("AnilistClient", "No data found for Anilist ID %d", id) + return nil, fmt.Errorf("no data found for Anilist ID %d", id) } - return nil, fmt.Errorf("failed after %d retries: %w", c.maxRetries, lastErr) + return &response, nil } diff --git a/utils/api/anilist/types.go b/utils/api/anilist/types.go index 367719b..4a49d57 100644 --- a/utils/api/anilist/types.go +++ b/utils/api/anilist/types.go @@ -1,207 +1,12 @@ package anilist -// AnilistAnimeResponse represents the response from AniList API -type AnilistAnimeResponse struct { - Data struct { - Media struct { - ID int `json:"id"` - MALID int `json:"idMal"` - Title struct { - Romaji string `json:"romaji"` - English string `json:"english"` - Native string `json:"native"` - UserPreferred string `json:"userPreferred"` - } `json:"title"` - Type string `json:"type"` - Format string `json:"format"` - Status string `json:"status"` - Description string `json:"description"` - StartDate struct { - Year int `json:"year"` - Month int `json:"month"` - Day int `json:"day"` - } `json:"startDate"` - EndDate struct { - Year int `json:"year"` - Month int `json:"month"` - Day int `json:"day"` - } `json:"endDate"` - Season string `json:"season"` - SeasonYear int `json:"seasonYear"` - Episodes int `json:"episodes"` - Duration int `json:"duration"` - Chapters int `json:"chapters"` - Volumes int `json:"volumes"` - CountryOfOrigin string `json:"countryOfOrigin"` - IsLicensed bool `json:"isLicensed"` - Source string `json:"source"` - Hashtag string `json:"hashtag"` - Trailer struct { - ID string `json:"id"` - Site string `json:"site"` - Thumbnail string `json:"thumbnail"` - } `json:"trailer"` - CoverImage struct { - ExtraLarge string `json:"extraLarge"` - Large string `json:"large"` - Medium string `json:"medium"` - Color string `json:"color"` - } `json:"coverImage"` - BannerImage string `json:"bannerImage"` - Genres []string `json:"genres"` - Synonyms []string `json:"synonyms"` - AverageScore int `json:"averageScore"` - MeanScore int `json:"meanScore"` - Popularity int `json:"popularity"` - IsLocked bool `json:"isLocked"` - Trending int `json:"trending"` - Favorites int `json:"favorites"` - Tags []struct { - ID int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Category string `json:"category"` - Rank int `json:"rank"` - IsGeneralSpoiler bool `json:"isGeneralSpoiler"` - IsMediaSpoiler bool `json:"isMediaSpoiler"` - IsAdult bool `json:"isAdult"` - } `json:"tags"` - Relations struct { - Edges []struct { - ID int `json:"id"` - RelationType string `json:"relationType"` - Node struct { - ID int `json:"id"` - Title struct { - Romaji string `json:"romaji"` - English string `json:"english"` - Native string `json:"native"` - UserPreferred string `json:"userPreferred"` - } `json:"title"` - Format string `json:"format"` - Type string `json:"type"` - Status string `json:"status"` - CoverImage struct { - ExtraLarge string `json:"extraLarge"` - Large string `json:"large"` - Medium string `json:"medium"` - Color string `json:"color"` - } `json:"coverImage"` - BannerImage string `json:"bannerImage"` - } `json:"node"` - } `json:"edges"` - } `json:"relations"` - Characters struct { - Edges []struct { - Role string `json:"role"` - Node struct { - ID int `json:"id"` - Name struct { - First string `json:"first"` - Last string `json:"last"` - Middle string `json:"middle"` - Full string `json:"full"` - Native string `json:"native"` - UserPreferred string `json:"userPreferred"` - } `json:"name"` - Image struct { - Large string `json:"large"` - Medium string `json:"medium"` - } `json:"image"` - Description string `json:"description"` - Age string `json:"age"` - } `json:"node"` - } `json:"edges"` - } `json:"characters"` - Staff struct { - Edges []struct { - Role string `json:"role"` - Node struct { - ID int `json:"id"` - Name struct { - First string `json:"first"` - Last string `json:"last"` - Middle string `json:"middle"` - Full string `json:"full"` - Native string `json:"native"` - UserPreferred string `json:"userPreferred"` - } `json:"name"` - Image struct { - Large string `json:"large"` - Medium string `json:"medium"` - } `json:"image"` - Description string `json:"description"` - PrimaryOccupations []string `json:"primaryOccupations"` - Gender string `json:"gender"` - Age int `json:"age"` - LanguageV2 string `json:"languageV2"` - } `json:"node"` - } `json:"edges"` - } `json:"staff"` - Studios struct { - Edges []struct { - IsMain bool `json:"isMain"` - Node struct { - ID int `json:"id"` - Name string `json:"name"` - } `json:"node"` - } `json:"edges"` - } `json:"studios"` - IsAdult bool `json:"isAdult"` - NextAiringEpisode struct { - ID int `json:"id"` - AiringAt int `json:"airingAt"` - TimeUntilAiring int `json:"timeUntilAiring"` - Episode int `json:"episode"` - } `json:"nextAiringEpisode"` - AiringSchedule struct { - Nodes []struct { - ID int `json:"id"` - Episode int `json:"episode"` - AiringAt int `json:"airingAt"` - TimeUntilAiring int `json:"timeUntilAiring"` - } `json:"nodes"` - } `json:"airingSchedule"` - Trends struct { - Nodes []struct { - Date int `json:"date"` - Trending int `json:"trending"` - Popularity int `json:"popularity"` - InProgress int `json:"inProgress"` - } `json:"nodes"` - } `json:"trends"` - ExternalLinks []struct { - ID int `json:"id"` - URL string `json:"url"` - Site string `json:"site"` - } `json:"externalLinks"` - StreamingEpisodes []struct { - Title string `json:"title"` - Thumbnail string `json:"thumbnail"` - URL string `json:"url"` - Site string `json:"site"` - } `json:"streamingEpisodes"` - Rankings []struct { - ID int `json:"id"` - Rank int `json:"rank"` - Type string `json:"type"` - Format string `json:"format"` - Year int `json:"year"` - Season string `json:"season"` - AllTime bool `json:"allTime"` - Context string `json:"context"` - } `json:"rankings"` - Stats struct { - ScoreDistribution []struct { - Score int `json:"score"` - Amount int `json:"amount"` - } `json:"scoreDistribution"` - StatusDistribution []struct { - Status string `json:"status"` - Amount int `json:"amount"` - } `json:"statusDistribution"` - } `json:"stats"` - SiteURL string `json:"siteUrl"` - } `json:"media"` - } `json:"data"` +import ( + "net/http" + "time" +) + +type client struct { + httpClient *http.Client + maxRetries int + backoff time.Duration } diff --git a/utils/api/jikan/jikan.go b/utils/api/jikan/jikan.go index e94223e..0121659 100644 --- a/utils/api/jikan/jikan.go +++ b/utils/api/jikan/jikan.go @@ -304,391 +304,3 @@ func GetProducerByID(producerID int) (*types.JikanSingleProducerResponse, error) } return &response, nil } - -// var ( -// // Global Jikan rate limiters -// jikanPerSecLimiter = ratelimit.NewRateLimiter(3, time.Second) -// jikanPerMinLimiter = ratelimit.NewRateLimiter(60, time.Minute) -// jikanLimiter = ratelimit.NewMultiLimiter(jikanPerSecLimiter, jikanPerMinLimiter) -// ) - -// // JikanClient provides methods to interact with the Jikan API -// type JikanClient struct { -// client *http.Client -// maxRetries int -// baseBackoff time.Duration -// } - -// // NewJikanClient creates a new Jikan API client -// func NewJikanClient() *JikanClient { -// return &JikanClient{ -// client: &http.Client{ -// Timeout: 15 * time.Second, -// }, -// maxRetries: 3, -// baseBackoff: 1 * time.Second, -// } -// } - -// // WaitForRateLimit waits until a request can be made according to rate limiting rules -// func (c *JikanClient) WaitForRateLimit() { -// jikanLimiter.Wait() -// } - -// // makeRequest makes an HTTP request with retries and proper error handling -// func (c *JikanClient) makeRequest(ctx context.Context, url string) ([]byte, error) { -// var bodyBytes []byte -// var statusCode int - -// retries := 0 -// for retries <= c.maxRetries { -// // Wait for rate limiter before attempting request -// c.WaitForRateLimit() - -// // Create the request with timeout context -// req, err := http.NewRequestWithContext(ctx, "GET", url, nil) -// if err != nil { -// return nil, fmt.Errorf("failed to create request: %w", err) -// } - -// // Execute the request -// resp, err := c.client.Do(req) -// if err != nil { -// if retries < c.maxRetries { -// retries++ -// backoffTime := time.Duration(float64(c.baseBackoff) * math.Pow(2, float64(retries-1))) -// time.Sleep(backoffTime) -// continue -// } -// return nil, fmt.Errorf("failed to execute request after %d retries: %w", c.maxRetries, err) -// } -// defer resp.Body.Close() - -// statusCode = resp.StatusCode - -// // Handle rate limiting with exponential backoff -// if statusCode == http.StatusTooManyRequests { -// if retries < c.maxRetries { -// retries++ -// backoffTime := time.Duration(float64(c.baseBackoff) * math.Pow(1.5, float64(retries-1))) - -// // Respect Retry-After header if available -// if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" { -// if seconds, err := strconv.Atoi(retryAfter); err == nil { -// backoffTime = time.Duration(seconds) * time.Second -// } -// } - -// time.Sleep(backoffTime) -// continue -// } -// return nil, fmt.Errorf("rate limited after %d retries", c.maxRetries) -// } else if statusCode != http.StatusOK { -// if retries < c.maxRetries { -// retries++ -// backoffTime := time.Duration(float64(c.baseBackoff) * math.Pow(2, float64(retries-1))) -// time.Sleep(backoffTime) -// continue -// } -// return nil, fmt.Errorf("request failed with status: %d", statusCode) -// } - -// // Limit response body size to prevent memory issues -// bodyBytes, err = io.ReadAll(io.LimitReader(resp.Body, 10*1024*1024)) // 10MB limit -// if err != nil { -// if retries < c.maxRetries { -// retries++ -// backoffTime := time.Duration(float64(c.baseBackoff) * math.Pow(2, float64(retries-1))) -// time.Sleep(backoffTime) -// continue -// } -// return nil, fmt.Errorf("failed to read response body: %w", err) -// } - -// // Success, break the retry loop -// return bodyBytes, nil -// } - -// return nil, fmt.Errorf("exhausted all retries with status code: %d", statusCode) -// } - -// // GetAnime fetches basic anime information by MAL ID -// func (c *JikanClient) GetAnime(malID int) (*JikanAnimeResponse, error) { -// apiURL := fmt.Sprintf("https://api.jikan.moe/v4/anime/%d", malID) - -// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) -// defer cancel() - -// bodyBytes, err := c.makeRequest(ctx, apiURL) -// if err != nil { -// return nil, fmt.Errorf("failed to get anime data: %w", err) -// } - -// var animeResponse JikanAnimeResponse -// if err := json.Unmarshal(bodyBytes, &animeResponse); err != nil { -// return nil, fmt.Errorf("failed to decode response: %w", err) -// } - -// if animeResponse.Data.MALID == 0 { -// return nil, fmt.Errorf("no data found for MAL ID %d", malID) -// } - -// return &animeResponse, nil -// } - -// // GetFullAnime fetches detailed anime information by MAL ID -// func (c *JikanClient) GetFullAnime(malID int) (*JikanAnimeResponse, error) { -// apiURL := fmt.Sprintf("https://api.jikan.moe/v4/anime/%d/full", malID) - -// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) -// defer cancel() - -// bodyBytes, err := c.makeRequest(ctx, apiURL) -// if err != nil { -// // Fallback to curl if HTTP client fails -// var curlErr error -// bodyBytes, curlErr = c.makeRequestWithCurl(apiURL) -// if curlErr != nil { -// return nil, fmt.Errorf("failed to get anime full data via HTTP (%w) and curl (%v)", err, curlErr) -// } -// } - -// var animeResponse JikanAnimeResponse -// if err := json.Unmarshal(bodyBytes, &animeResponse); err != nil { -// return nil, fmt.Errorf("failed to decode response: %w", err) -// } - -// if animeResponse.Data.MALID == 0 { -// return nil, fmt.Errorf("no data found for MAL ID %d", malID) -// } - -// return &animeResponse, nil -// } - -// // makeRequestWithCurl uses curl as a fallback when Go HTTP client fails -// func (c *JikanClient) makeRequestWithCurl(url string) ([]byte, error) { -// c.WaitForRateLimit() - -// cmd := exec.Command("curl", "-s", "-H", "Accept: application/json", url) -// output, err := cmd.Output() -// if err != nil { -// return nil, fmt.Errorf("curl command failed: %w", err) -// } - -// return output, nil -// } - -// // GetAnimeEpisodes fetches all episodes for an anime by MAL ID -// func (c *JikanClient) GetAnimeEpisodes(malID int) (*JikanAnimeEpisodeResponse, error) { -// result := JikanAnimeEpisodeResponse{ -// Data: []JikanAnimeEpisode{}, -// } - -// maxPages := 25 // Safety limit to avoid excessive requests -// page := 1 -// maxAttempts := 15 // Maximum number of attempts across all pages -// totalAttempts := 0 - -// for page <= maxPages && totalAttempts < maxAttempts { -// apiURL := fmt.Sprintf("https://api.jikan.moe/v4/anime/%d/episodes?page=%d", malID, page) - -// ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) - -// totalAttempts++ - -// bodyBytes, err := c.makeRequest(ctx, apiURL) -// cancel() - -// if err != nil { -// // If we have some episodes already, return them rather than failing -// if len(result.Data) > 0 { -// result.Pagination.HasNextPage = false -// break -// } -// return nil, fmt.Errorf("failed to get anime episodes page %d: %w", page, err) -// } - -// var pageResponse JikanAnimeEpisodeResponse -// if err := json.Unmarshal(bodyBytes, &pageResponse); err != nil { -// // Return what we have if we got some pages successfully -// if len(result.Data) > 0 { -// result.Pagination.HasNextPage = false -// break -// } -// return nil, fmt.Errorf("failed to decode episodes response: %w", err) -// } - -// // Append episodes from this page -// result.Data = append(result.Data, pageResponse.Data...) -// result.Pagination = pageResponse.Pagination - -// // Check if we need to fetch more pages -// if !pageResponse.Pagination.HasNextPage { -// break -// } - -// page++ -// } - -// return &result, nil -// } - -// // GetAnimeCharacters fetches all characters for an anime by MAL ID -// func (c *JikanClient) GetAnimeCharacters(malID int) (*JikanAnimeCharacterResponse, error) { -// apiURL := fmt.Sprintf("https://api.jikan.moe/v4/anime/%d/characters", malID) - -// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) -// defer cancel() - -// bodyBytes, err := c.makeRequest(ctx, apiURL) -// if err != nil { -// return nil, fmt.Errorf("failed to get anime characters: %w", err) -// } - -// var characterResponse JikanAnimeCharacterResponse -// if err := json.Unmarshal(bodyBytes, &characterResponse); err != nil { -// return nil, fmt.Errorf("failed to decode characters response: %w", err) -// } - -// return &characterResponse, nil -// } - -// // GetAnimeGenres fetches all anime genres from MAL -// func (c *JikanClient) GetAnimeGenres() (*JikanGenresResponse, error) { -// apiURL := "https://api.jikan.moe/v4/genres/anime" - -// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) -// defer cancel() - -// bodyBytes, err := c.makeRequest(ctx, apiURL) -// if err != nil { -// return nil, fmt.Errorf("failed to get anime genres: %w", err) -// } - -// var genresResponse JikanGenresResponse -// if err := json.Unmarshal(bodyBytes, &genresResponse); err != nil { -// return nil, fmt.Errorf("failed to decode genres response: %w", err) -// } - -// return &genresResponse, nil -// } - -// // GetAnimeByGenre fetches paginated anime list for a specific genre -// func (c *JikanClient) GetAnimeByGenre(genreID int, page int, limit int) (*JikanAnimeListResponse, error) { -// apiURL := fmt.Sprintf("https://api.jikan.moe/v4/anime?genres=%d&page=%d&limit=%d", genreID, page, limit) - -// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) -// defer cancel() - -// bodyBytes, err := c.makeRequest(ctx, apiURL) -// if err != nil { -// return nil, fmt.Errorf("failed to get anime by genre: %w", err) -// } - -// var listResponse JikanAnimeListResponse -// if err := json.Unmarshal(bodyBytes, &listResponse); err != nil { -// return nil, fmt.Errorf("failed to decode anime list response: %w", err) -// } - -// return &listResponse, nil -// } - -// // GetAnimeProducers fetches all producers from Jikan API (paginated) -// func (c *JikanClient) GetAnimeProducers(page int) (*JikanProducersFullResponse, error) { -// apiURL := fmt.Sprintf("https://api.jikan.moe/v4/producers?page=%d", page) - -// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) -// defer cancel() - -// bodyBytes, err := c.makeRequest(ctx, apiURL) -// if err != nil { -// return nil, fmt.Errorf("failed to get producers: %w", err) -// } - -// var response JikanProducersFullResponse -// if err := json.Unmarshal(bodyBytes, &response); err != nil { -// return nil, fmt.Errorf("failed to decode producers response: %w", err) -// } - -// return &response, nil -// } - -// // GetProducerExternal fetches external URLs for a specific producer -// func (c *JikanClient) GetProducerExternal(producerID int) (*JikanProducerExternalResponse, error) { -// apiURL := fmt.Sprintf("https://api.jikan.moe/v4/producers/%d/external", producerID) - -// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) -// defer cancel() - -// bodyBytes, err := c.makeRequest(ctx, apiURL) -// if err != nil { -// return nil, fmt.Errorf("failed to get producer external: %w", err) -// } - -// var response JikanProducerExternalResponse -// if err := json.Unmarshal(bodyBytes, &response); err != nil { -// return nil, fmt.Errorf("failed to decode producer external response: %w", err) -// } - -// return &response, nil -// } - -// // GetAnimeByProducer fetches paginated anime list by producer ID -// func (c *JikanClient) GetAnimeByProducer(producerID int, page int, limit int) (*JikanAnimeListResponse, error) { -// apiURL := fmt.Sprintf("https://api.jikan.moe/v4/anime?producers=%d&page=%d&limit=%d", producerID, page, limit) - -// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) -// defer cancel() - -// bodyBytes, err := c.makeRequest(ctx, apiURL) -// if err != nil { -// return nil, fmt.Errorf("failed to get anime by producer: %w", err) -// } - -// var listResponse JikanAnimeListResponse -// if err := json.Unmarshal(bodyBytes, &listResponse); err != nil { -// return nil, fmt.Errorf("failed to decode anime list response: %w", err) -// } - -// return &listResponse, nil -// } - -// // GetAnimeByStudio fetches paginated anime list by studio ID (uses producers endpoint) -// func (c *JikanClient) GetAnimeByStudio(studioID int, page int, limit int) (*JikanAnimeListResponse, error) { -// apiURL := fmt.Sprintf("https://api.jikan.moe/v4/anime?producers=%d&page=%d&limit=%d", studioID, page, limit) - -// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) -// defer cancel() - -// bodyBytes, err := c.makeRequest(ctx, apiURL) -// if err != nil { -// return nil, fmt.Errorf("failed to get anime by studio: %w", err) -// } - -// var listResponse JikanAnimeListResponse -// if err := json.Unmarshal(bodyBytes, &listResponse); err != nil { -// return nil, fmt.Errorf("failed to decode anime list response: %w", err) -// } - -// return &listResponse, nil -// } - -// // GetAnimeByLicensor fetches paginated anime list by licensor ID (uses producers endpoint) -// func (c *JikanClient) GetAnimeByLicensor(licensorID int, page int, limit int) (*JikanAnimeListResponse, error) { -// apiURL := fmt.Sprintf("https://api.jikan.moe/v4/anime?producers=%d&page=%d&limit=%d", licensorID, page, limit) - -// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) -// defer cancel() - -// bodyBytes, err := c.makeRequest(ctx, apiURL) -// if err != nil { -// return nil, fmt.Errorf("failed to get anime by licensor: %w", err) -// } - -// var listResponse JikanAnimeListResponse -// if err := json.Unmarshal(bodyBytes, &listResponse); err != nil { -// return nil, fmt.Errorf("failed to decode anime list response: %w", err) -// } - -// return &listResponse, nil -// } |
