aboutsummaryrefslogtreecommitdiff
path: root/utils/api
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-02-05 15:56:01 +0530
committerBobby <[email protected]>2026-02-05 15:56:01 +0530
commit111ddd8b5fca2612256a7bd31781c149f10f83d8 (patch)
tree23256435338a22f31f3dc52331ae2f04705ec1f8 /utils/api
parentb0f01eea9d61aa4d05b0fe253c8a32e35fa95e28 (diff)
downloadmetachan-111ddd8b5fca2612256a7bd31781c149f10f83d8.tar.xz
metachan-111ddd8b5fca2612256a7bd31781c149f10f83d8.zip
Refactor Jikan API types: remove unused structures and add HTTP client configuration
Diffstat (limited to 'utils/api')
-rw-r--r--utils/api/jikan/jikan.go784
-rw-r--r--utils/api/jikan/types.go247
2 files changed, 597 insertions, 434 deletions
diff --git a/utils/api/jikan/jikan.go b/utils/api/jikan/jikan.go
index 4f34dc3..e94223e 100644
--- a/utils/api/jikan/jikan.go
+++ b/utils/api/jikan/jikan.go
@@ -3,300 +3,692 @@ package jikan
import (
"context"
"encoding/json"
+ "errors"
"fmt"
"io"
"math"
+ "metachan/types"
+ "metachan/utils/logger"
"metachan/utils/ratelimit"
"net/http"
- "os/exec"
"strconv"
"time"
)
+const (
+ jikanAPIBaseURL = "https://api.jikan.moe/v4"
+ rateLimitPerSec = 3
+ rateLimitPerMin = 60
+ contextTimeout = 60 * time.Second
+)
+
var (
- // Global Jikan rate limiters
- jikanPerSecLimiter = ratelimit.NewRateLimiter(3, time.Second)
- jikanPerMinLimiter = ratelimit.NewRateLimiter(60, time.Minute)
- jikanLimiter = ratelimit.NewMultiLimiter(jikanPerSecLimiter, jikanPerMinLimiter)
+ rateLimiter = ratelimit.NewMultiLimiter(
+ ratelimit.NewRateLimiter(rateLimitPerSec, time.Second),
+ ratelimit.NewRateLimiter(rateLimitPerMin, time.Minute),
+ )
+ clientInstance = &client{
+ httpClient: &http.Client{
+ Timeout: 15 * time.Second,
+ },
+ maxRetries: 3,
+ backoff: 1 * time.Second,
+ }
)
-// JikanClient provides methods to interact with the Jikan API
-type JikanClient struct {
- client *http.Client
- maxRetries int
- baseBackoff time.Duration
+func (c *client) getBackOffDuration(attempt int) time.Duration {
+ return time.Duration(float64(c.backoff) * math.Pow(2, float64(attempt-1)))
}
-// 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,
+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
}
-// WaitForRateLimit waits until a request can be made according to rate limiting rules
-func (c *JikanClient) WaitForRateLimit() {
- jikanLimiter.Wait()
+func (c *client) handleRetry(retries *int, url string, reason string, retryAfter time.Duration) bool {
+ *retries++
+ if *retries >= c.maxRetries {
+ return false
+ }
+
+ backoffDuration := c.getBackOffDuration(*retries)
+ if retryAfter > backoffDuration {
+ backoffDuration = retryAfter
+ }
+
+ logger.Warnf("JikanClient", "%s for %s (attempt %d/%d)", reason, url, *retries, c.maxRetries)
+ time.Sleep(backoffDuration)
+ return true
}
-// 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
+func (c *client) makeRequest(ctx context.Context, url string) ([]byte, error) {
+ var response *http.Response
+ var retries int
- retries := 0
- for retries <= c.maxRetries {
- // Wait for rate limiter before attempting request
- c.WaitForRateLimit()
+ for retries < c.maxRetries {
+ rateLimiter.Wait()
- // Create the request with timeout context
- req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
+ request, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
- return nil, fmt.Errorf("failed to create request: %w", err)
+ logger.Errorf("JikanClient", "Failed to create request: %v", err)
+ return nil, errors.New("failed to create request to Jikan API")
}
- // Execute the request
- resp, err := c.client.Do(req)
+ response, err = c.httpClient.Do(request)
if err != nil {
- if retries < c.maxRetries {
- retries++
- backoffTime := time.Duration(float64(c.baseBackoff) * math.Pow(2, float64(retries-1)))
- time.Sleep(backoffTime)
- continue
+ if !c.handleRetry(&retries, url, fmt.Sprintf("Request failed: %v", err), 0) {
+ logger.Errorf("JikanClient", "All retries exhausted for request to %s: %v", url, err)
+ return nil, errors.New("failed to make request to Jikan API after max retries")
}
- return nil, fmt.Errorf("failed to execute request after %d retries: %w", c.maxRetries, err)
+ continue
}
- 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
- }
- }
+ defer response.Body.Close()
- time.Sleep(backoffTime)
- continue
+ switch response.StatusCode {
+ case http.StatusTooManyRequests:
+ retryAfter := c.getRetryAfterDuration(response)
+ if !c.handleRetry(&retries, url, "Rate limited", retryAfter) {
+ logger.Errorf("JikanClient", "All retries exhausted for request to %s", url)
+ return nil, errors.New("failed to make request to Jikan API after max retries")
}
- 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)
- }
+ case http.StatusOK:
+ bytes, err := io.ReadAll(response.Body)
- // 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
+ if err != nil {
+ logger.Errorf("JikanClient", "Failed to read response body from %s: %v", url, err)
+ return nil, errors.New("failed to read response from Jikan API")
}
- return nil, fmt.Errorf("failed to read response body: %w", err)
- }
- // Success, break the retry loop
- return bodyBytes, nil
+ return bytes, nil
+ default:
+ retries++
+ backoffDuration := c.getBackOffDuration(retries)
+
+ logger.Warnf("JikanClient", "Request to %s returned status %d (attempt %d/%d)", url, response.StatusCode, retries, c.maxRetries)
+
+ time.Sleep(backoffDuration)
+ }
}
- return nil, fmt.Errorf("exhausted all retries with status code: %d", statusCode)
+ logger.Errorf("JikanClient", "All retries exhausted for request to %s", url)
+ return nil, errors.New("failed to make request to Jikan API after max retries")
}
-// 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)
+func GetAnimeByMALID(id int) (*types.JikanAnimeResponse, error) {
+ url := fmt.Sprintf("%s/anime/%d/full", jikanAPIBaseURL, id)
+ ctx, cancel := context.WithTimeout(context.Background(), contextTimeout)
- ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
- bodyBytes, err := c.makeRequest(ctx, apiURL)
+ bytes, err := clientInstance.makeRequest(ctx, url)
if err != nil {
- return nil, fmt.Errorf("failed to get anime data: %w", err)
+ logger.Errorf("JikanClient", "GetAnimeByMALID failed for ID %d: %v", id, err)
+ return nil, errors.New("failed to fetch anime data from Jikan API")
}
- var animeResponse JikanAnimeResponse
- if err := json.Unmarshal(bodyBytes, &animeResponse); err != nil {
- return nil, fmt.Errorf("failed to decode response: %w", err)
+ var response types.JikanAnimeResponse
+ if err := json.Unmarshal(bytes, &response); err != nil {
+ logger.Errorf("JikanClient", "Failed to unmarshal response for ID %d: %v", id, err)
+ return nil, errors.New("failed to parse anime data from Jikan API")
}
- if animeResponse.Data.MALID == 0 {
- return nil, fmt.Errorf("no data found for MAL ID %d", malID)
+ return &response, nil
+}
+
+func GetAnimeEpisodesByMALID(id int) (*types.JikanAnimeEpisodeResponse, error) {
+ url := fmt.Sprintf("%s/anime/%d/episodes", jikanAPIBaseURL, id)
+
+ page := 1
+ hasNextPage := true
+
+ response := &types.JikanAnimeEpisodeResponse{
+ Pagination: types.JikanGenericPaginationEntity{},
+ Data: []types.JikanAnimeSingleEpisode{},
}
- return &animeResponse, nil
-}
+ for hasNextPage {
+ ctx, cancel := context.WithTimeout(context.Background(), contextTimeout)
+ pageURL := fmt.Sprintf("%s?page=%d", url, page)
-// 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)
+ bytes, err := clientInstance.makeRequest(ctx, pageURL)
- ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
- defer cancel()
+ 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)
+ if err != nil {
+ logger.Errorf("JikanClient", "GetAnimeEpisodesByMALID failed for ID %d on page %d: %v", id, page, err)
+ return nil, errors.New("failed to fetch anime episodes from Jikan API")
}
+
+ var pageResponse types.JikanAnimeEpisodeResponse
+
+ if err := json.Unmarshal(bytes, &pageResponse); err != nil {
+ logger.Errorf("JikanClient", "Failed to unmarshal episodes response for ID %d on page %d: %v", id, page, err)
+ return nil, errors.New("failed to parse anime episodes from Jikan API")
+ }
+
+ if response.Pagination.LastVisiblePage == 0 {
+ response.Pagination = pageResponse.Pagination
+ }
+
+ response.Data = append(response.Data, pageResponse.Data...)
+ hasNextPage = pageResponse.Pagination.HasNextPage
+ page++
}
- var animeResponse JikanAnimeResponse
- if err := json.Unmarshal(bodyBytes, &animeResponse); err != nil {
- return nil, fmt.Errorf("failed to decode response: %w", err)
+ return response, nil
+}
+
+func GetAnimeCharactersByMALID(id int) (*types.JikanAnimeCharacterResponse, error) {
+ url := fmt.Sprintf("%s/anime/%d/characters", jikanAPIBaseURL, id)
+ ctx, cancel := context.WithTimeout(context.Background(), contextTimeout)
+
+ defer cancel()
+
+ bytes, err := clientInstance.makeRequest(ctx, url)
+ if err != nil {
+ logger.Errorf("JikanClient", "GetAnimeCharactersByMALID failed for ID %d: %v", id, err)
+ return nil, errors.New("failed to fetch anime characters from Jikan API")
}
- if animeResponse.Data.MALID == 0 {
- return nil, fmt.Errorf("no data found for MAL ID %d", malID)
+ var response types.JikanAnimeCharacterResponse
+ if err := json.Unmarshal(bytes, &response); err != nil {
+ logger.Errorf("JikanClient", "Failed to unmarshal characters response for ID %d: %v", id, err)
+ return nil, errors.New("failed to parse anime characters from Jikan API")
}
- return &animeResponse, nil
+ return &response, nil
}
-// makeRequestWithCurl uses curl as a fallback when Go HTTP client fails
-func (c *JikanClient) makeRequestWithCurl(url string) ([]byte, error) {
- c.WaitForRateLimit()
+func GetAnimeGenres() (*types.JikanGenresResponse, error) {
+ url := fmt.Sprintf("%s/genres/anime", jikanAPIBaseURL)
+ ctx, cancel := context.WithTimeout(context.Background(), contextTimeout)
+
+ defer cancel()
- cmd := exec.Command("curl", "-s", "-H", "Accept: application/json", url)
- output, err := cmd.Output()
+ bytes, err := clientInstance.makeRequest(ctx, url)
if err != nil {
- return nil, fmt.Errorf("curl command failed: %w", err)
+ logger.Errorf("JikanClient", "GetAnimeGenres failed: %v", err)
+ return nil, errors.New("failed to fetch anime genres from Jikan API")
}
- return output, nil
+ var response types.JikanGenresResponse
+ if err := json.Unmarshal(bytes, &response); err != nil {
+ logger.Errorf("JikanClient", "Failed to unmarshal genres response: %v", err)
+ return nil, errors.New("failed to parse anime genres from Jikan API")
+ }
+
+ return &response, nil
}
-// GetAnimeEpisodes fetches all episodes for an anime by MAL ID
-func (c *JikanClient) GetAnimeEpisodes(malID int) (*JikanAnimeEpisodeResponse, error) {
- result := JikanAnimeEpisodeResponse{
- Data: []JikanAnimeEpisode{},
+func GetAnimeByGenre(genreID int, page int, limit int) (*types.JikanAnimeSearchResponse, error) {
+ url := fmt.Sprintf("%s/anime?genres=%d&page=%d&limit=%d", jikanAPIBaseURL, genreID, page, limit)
+ ctx, cancel := context.WithTimeout(context.Background(), contextTimeout)
+
+ defer cancel()
+
+ bytes, err := clientInstance.makeRequest(ctx, url)
+ if err != nil {
+ logger.Errorf("JikanClient", "GetAnimeByGenre failed for genre %d: %v", genreID, err)
+ return nil, errors.New("failed to fetch anime by genre from Jikan API")
}
- maxPages := 25 // Safety limit to avoid excessive requests
- page := 1
- maxAttempts := 15 // Maximum number of attempts across all pages
- totalAttempts := 0
+ var response types.JikanAnimeSearchResponse
+ if err := json.Unmarshal(bytes, &response); err != nil {
+ logger.Errorf("JikanClient", "Failed to unmarshal anime by genre response for genre %d: %v", genreID, err)
+ return nil, errors.New("failed to parse anime by genre from Jikan API")
+ }
- for page <= maxPages && totalAttempts < maxAttempts {
- apiURL := fmt.Sprintf("https://api.jikan.moe/v4/anime/%d/episodes?page=%d", malID, page)
+ return &response, nil
+}
- ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
+func GetAnimeProducers() (*types.JikanProducersResponse, error) {
+ url := fmt.Sprintf("%s/producers", jikanAPIBaseURL)
+ ctx, cancel := context.WithTimeout(context.Background(), contextTimeout)
+ page := 1
+ hasNextPage := true
- totalAttempts++
+ response := &types.JikanProducersResponse{
+ Data: []types.JikanSingleProducer{},
+ Pagination: types.JikanGenericPaginationEntity{},
+ }
- bodyBytes, err := c.makeRequest(ctx, apiURL)
- cancel()
+ defer cancel()
+ for hasNextPage {
+ pageURL := fmt.Sprintf("%s?page=%d", url, page)
+
+ bytes, err := clientInstance.makeRequest(ctx, pageURL)
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)
+ logger.Errorf("JikanClient", "GetAnimeProducers failed on page %d: %v", page, err)
+ return nil, errors.New("failed to fetch anime producers from Jikan API")
}
- 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)
+ var pageResponse types.JikanProducersResponse
+ if err := json.Unmarshal(bytes, &pageResponse); err != nil {
+ logger.Errorf("JikanClient", "Failed to unmarshal producers response on page %d: %v", page, err)
+ return nil, errors.New("failed to parse anime producers from Jikan API")
}
- // 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
+ if response.Pagination.LastVisiblePage == 0 {
+ response.Pagination = pageResponse.Pagination
}
+ response.Data = append(response.Data, pageResponse.Data...)
+ hasNextPage = pageResponse.Pagination.HasNextPage
page++
}
- return &result, nil
+ return response, 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)
+func GetProducerByID(producerID int) (*types.JikanSingleProducerResponse, error) {
+ url := fmt.Sprintf("%s/producers/%d/full", jikanAPIBaseURL, producerID)
+ ctx, cancel := context.WithTimeout(context.Background(), contextTimeout)
- ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
-
- bodyBytes, err := c.makeRequest(ctx, apiURL)
+ bytes, err := clientInstance.makeRequest(ctx, url)
if err != nil {
- return nil, fmt.Errorf("failed to get anime characters: %w", err)
+ logger.Errorf("JikanClient", "GetProducerByID failed for ID %d: %v", producerID, err)
+ return nil, errors.New("failed to fetch producer data from Jikan API")
}
- var characterResponse JikanAnimeCharacterResponse
- if err := json.Unmarshal(bodyBytes, &characterResponse); err != nil {
- return nil, fmt.Errorf("failed to decode characters response: %w", err)
+ var response types.JikanSingleProducerResponse
+ if err := json.Unmarshal(bytes, &response); err != nil {
+ logger.Errorf("JikanClient", "Failed to unmarshal producer response for ID %d: %v", producerID, err)
+ return nil, errors.New("failed to parse producer data from Jikan API")
}
-
- return &characterResponse, nil
+ return &response, nil
}
-// GetAnimeGenres fetches all anime genres from MAL
-func (c *JikanClient) GetAnimeGenres() (*JikanGenresResponse, error) {
- apiURL := "https://api.jikan.moe/v4/genres/anime"
+// 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()
+// 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)
- }
+// bodyBytes, err := c.makeRequest(ctx, apiURL)
+// if err != nil {
+// return nil, fmt.Errorf("failed to get producer external: %w", err)
+// }
- var genresResponse JikanGenresResponse
- if err := json.Unmarshal(bodyBytes, &genresResponse); err != nil {
- return nil, fmt.Errorf("failed to decode genres response: %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 &genresResponse, nil
-}
+// return &response, 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)
+// // 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()
+// 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)
- }
+// 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)
- }
+// 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
-}
+// 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
+// }
diff --git a/utils/api/jikan/types.go b/utils/api/jikan/types.go
index 6e7d7f7..3326f89 100644
--- a/utils/api/jikan/types.go
+++ b/utils/api/jikan/types.go
@@ -1,241 +1,12 @@
package jikan
-// JikanPagination represents the pagination data in Jikan API responses
-type JikanPagination struct {
- LastVisiblePage int `json:"last_visible_page"`
- HasNextPage bool `json:"has_next_page"`
-}
-
-// JikanGenericMALStructure represents a common structure for various MAL entities
-type JikanGenericMALStructure struct {
- MALID int `json:"mal_id"`
- Type string `json:"type"`
- URL string `json:"url"`
- Name string `json:"name"`
-}
-
-// JikanGenre represents a genre from Jikan genres API
-type JikanGenre struct {
- MALID int `json:"mal_id"`
- Name string `json:"name"`
- URL string `json:"url"`
- Count int `json:"count"`
-}
-
-// JikanGenresResponse represents the genres response from Jikan API
-type JikanGenresResponse struct {
- Data []JikanGenre `json:"data"`
-}
-
-// JikanAnimeListItem represents a single anime in list responses
-type JikanAnimeListItem struct {
- MALID int `json:"mal_id"`
- URL string `json:"url"`
- Title string `json:"title"`
- TitleEnglish string `json:"title_english"`
- TitleJapanese string `json:"title_japanese"`
- TitleSynonyms []string `json:"title_synonyms"`
- Type string `json:"type"`
- Source string `json:"source"`
- Episodes int `json:"episodes"`
- Status string `json:"status"`
- Airing bool `json:"airing"`
- Synopsis string `json:"synopsis"`
- Score float64 `json:"score"`
- ScoredBy int `json:"scored_by"`
- Rank int `json:"rank"`
- Popularity int `json:"popularity"`
- Members int `json:"members"`
- Favorites int `json:"favorites"`
- Season string `json:"season"`
- Year int `json:"year"`
- Images struct {
- JPG struct {
- ImageURL string `json:"image_url"`
- SmallImageURL string `json:"small_image_url"`
- LargeImageURL string `json:"large_image_url"`
- } `json:"jpg"`
- } `json:"images"`
- Genres []JikanGenericMALStructure `json:"genres"`
- ExplicitGenres []JikanGenericMALStructure `json:"explicit_genres"`
- Producers []JikanGenericMALStructure `json:"producers"`
- Licensors []JikanGenericMALStructure `json:"licensors"`
- Studios []JikanGenericMALStructure `json:"studios"`
-}
-
-// JikanAnimeListResponse represents paginated anime list response
-type JikanAnimeListResponse struct {
- Pagination struct {
- LastVisiblePage int `json:"last_visible_page"`
- HasNextPage bool `json:"has_next_page"`
- CurrentPage int `json:"current_page"`
- Items struct {
- Count int `json:"count"`
- Total int `json:"total"`
- PerPage int `json:"per_page"`
- } `json:"items"`
- } `json:"pagination"`
- Data []JikanAnimeListItem `json:"data"`
-}
-
-// JikanAnimeResponse represents the main anime response from Jikan API
-type JikanAnimeResponse struct {
- Data struct {
- MALID int `json:"mal_id"`
- URL string `json:"url"`
- Images struct {
- JPG struct {
- ImageURL string `json:"image_url"`
- SmallImageURL string `json:"small_image_url"`
- LargeImageURL string `json:"large_image_url"`
- } `json:"jpg"`
- WebP struct {
- ImageURL string `json:"image_url"`
- SmallImageURL string `json:"small_image_url"`
- LargeImageURL string `json:"large_image_url"`
- } `json:"webp"`
- } `json:"images"`
- Trailer struct {
- YoutubeID string `json:"youtube_id"`
- URL string `json:"url"`
- EmbedURL string `json:"embed_url"`
- Images struct {
- ImageURL string `json:"image_url"`
- SmallImageURL string `json:"small_image_url"`
- MediumImageURL string `json:"medium_image_url"`
- LargeImageURL string `json:"large_image_url"`
- MaximumImageURL string `json:"maximum_image_url"`
- } `json:"images"`
- } `json:"trailer"`
- Approved bool `json:"approved"`
- Titles []struct {
- Type string `json:"type"`
- Title string `json:"title"`
- } `json:"titles"`
- Title string `json:"title"`
- TitleEnglish string `json:"title_english"`
- TitleJapanese string `json:"title_japanese"`
- TitleSynonyms []string `json:"title_synonyms"`
- Type string `json:"type"`
- Source string `json:"source"`
- Episodes int `json:"episodes"`
- Status string `json:"status"`
- Airing bool `json:"airing"`
- Aired struct {
- From string `json:"from"`
- To string `json:"to"`
- Prop struct {
- From struct {
- Day int `json:"day"`
- Month int `json:"month"`
- Year int `json:"year"`
- } `json:"from"`
- To struct {
- Day int `json:"day"`
- Month int `json:"month"`
- Year int `json:"year"`
- } `json:"to"`
- } `json:"prop"`
- String string `json:"string"`
- } `json:"aired"`
- Duration string `json:"duration"`
- Rating string `json:"rating"`
- Score float64 `json:"score"`
- ScoredBy int `json:"scored_by"`
- Rank int `json:"rank"`
- Popularity int `json:"popularity"`
- Members int `json:"members"`
- Favorites int `json:"favorites"`
- Synopsis string `json:"synopsis"`
- Background string `json:"background"`
- Season string `json:"season"`
- Year int `json:"year"`
- Broadcast struct {
- Day string `json:"day"`
- Time string `json:"time"`
- Timezone string `json:"timezone"`
- String string `json:"string"`
- } `json:"broadcast"`
- Producers []JikanGenericMALStructure `json:"producers"`
- Licensors []JikanGenericMALStructure `json:"licensors"`
- Studios []JikanGenericMALStructure `json:"studios"`
- Genres []JikanGenericMALStructure `json:"genres"`
- ExplicitGenres []JikanGenericMALStructure `json:"explicit_genres"`
- Themes []JikanGenericMALStructure `json:"themes"`
- Demographics []JikanGenericMALStructure `json:"demographics"`
- Relations []struct {
- Relation string `json:"relation"`
- Entry []JikanGenericMALStructure `json:"entry"`
- } `json:"relations"`
- Theme struct {
- Openings []string `json:"openings"`
- Endings []string `json:"endings"`
- } `json:"theme"`
- External []struct {
- Name string `json:"name"`
- URL string `json:"url"`
- } `json:"external"`
- Streaming []struct {
- Name string `json:"name"`
- URL string `json:"url"`
- } `json:"streaming"`
- } `json:"data"`
-}
-
-// JikanAnimeEpisode represents an episode from Jikan API
-type JikanAnimeEpisode struct {
- MALID int `json:"mal_id"`
- URL string `json:"url"`
- Title string `json:"title"`
- TitleJapanese string `json:"title_japanese"`
- TitleRomaji string `json:"title_romaji"`
- Aired string `json:"aired"`
- Score float64 `json:"score"`
- Filler bool `json:"filler"`
- Recap bool `json:"recap"`
- ForumURL string `json:"forum_url"`
-}
-
-// JikanAnimeEpisodeResponse represents the episodes response from Jikan API
-type JikanAnimeEpisodeResponse struct {
- Pagination JikanPagination `json:"pagination"`
- Data []JikanAnimeEpisode `json:"data"`
-}
-
-// JikanAnimeCharacterResponse represents the characters response from Jikan API
-type JikanAnimeCharacterResponse struct {
- Data []struct {
- Character struct {
- MALID int `json:"mal_id"`
- URL string `json:"url"`
- Images struct {
- JPG struct {
- ImageURL string `json:"image_url"`
- SmallImageURL string `json:"small_image_url"`
- } `json:"jpg"`
- WebP struct {
- ImageURL string `json:"image_url"`
- SmallImageURL string `json:"small_image_url"`
- } `json:"webp"`
- } `json:"images"`
- Name string `json:"name"`
- } `json:"character"`
- Role string `json:"role"`
- VoiceActors []struct {
- Person struct {
- MALID int `json:"mal_id"`
- URL string `json:"url"`
- Images struct {
- JPG struct {
- ImageURL string `json:"image_url"`
- } `json:"jpg"`
- WebP struct {
- ImageURL string `json:"image_url"`
- } `json:"webp"`
- } `json:"images"`
- Name string `json:"name"`
- } `json:"person"`
- Language string `json:"language"`
- } `json:"voice_actors"`
- } `json:"data"`
+import (
+ "net/http"
+ "time"
+)
+
+type client struct {
+ httpClient *http.Client
+ maxRetries int
+ backoff time.Duration
}