diff options
| author | Bobby <[email protected]> | 2025-05-06 22:41:12 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2025-05-06 22:41:12 +0530 |
| commit | 8cc063b4f65c2181fde4c567f2cdc82a75cca9bf (patch) | |
| tree | ed313f5f694491467e727a9e604531c9dc0a3ecf /utils | |
| parent | 8fa6d4dd33abe412bb09af949810ee0a1f9678bf (diff) | |
| download | metachan-8cc063b4f65c2181fde4c567f2cdc82a75cca9bf.tar.xz metachan-8cc063b4f65c2181fde4c567f2cdc82a75cca9bf.zip | |
additional data and seasons
Diffstat (limited to 'utils')
| -rw-r--r-- | utils/anime/anime.go | 135 | ||||
| -rw-r--r-- | utils/anime/jikan.go | 115 | ||||
| -rw-r--r-- | utils/anime/tvdb.go | 163 |
3 files changed, 411 insertions, 2 deletions
diff --git a/utils/anime/anime.go b/utils/anime/anime.go index 885c9f9..ab10323 100644 --- a/utils/anime/anime.go +++ b/utils/anime/anime.go @@ -10,7 +10,7 @@ import ( func GetAnimeDetails(animeMapping *entities.AnimeMapping) (*types.Anime, error) { malID := animeMapping.MAL - anime, err := getAnimeViaJikan(malID) + anime, err := getFullAnimeViaJikan(malID) if err != nil { return nil, fmt.Errorf("failed to get anime details: %w", err) } @@ -46,6 +46,28 @@ func GetAnimeDetails(animeMapping *entities.AnimeMapping) (*types.Anime, error) logos = types.AnimeLogos{} } + // Get seasons information if TVDB ID is available + var seasons []types.AnimeSeason + if animeMapping.TVDB != 0 { + // Get all mappings for this TVDB ID (representing different seasons) + seasonMappings, err := FindSeasonMappings(animeMapping.TVDB) + if err != nil { + logger.Log(fmt.Sprintf("Failed to find season mappings: %v", err), types.LogOptions{ + Level: types.Warn, + Prefix: "AnimeAPI", + }) + } else if len(seasonMappings) > 0 { + // Process the season mappings to get season details + seasons, err = GetAnimeSeason(&seasonMappings, malID) + if err != nil { + logger.Log(fmt.Sprintf("Failed to get anime seasons: %v", err), types.LogOptions{ + Level: types.Warn, + Prefix: "AnimeAPI", + }) + } + } + } + animeDetails := &types.Anime{ MALID: malID, Titles: types.AnimeTitles{ @@ -57,9 +79,57 @@ func GetAnimeDetails(animeMapping *entities.AnimeMapping) (*types.Anime, error) Synopsis: anime.Data.Synopsis, Type: types.AniSyncType(animeMapping.Type), Source: anime.Data.Source, + Airing: anime.Data.Airing, Status: anime.Data.Status, + AiringStatus: types.AiringStatus{ + From: types.AiringStatusDates{ + Day: anime.Data.Aired.Prop.From.Day, + Month: anime.Data.Aired.Prop.From.Month, + Year: anime.Data.Aired.Prop.From.Year, + String: anime.Data.Aired.From, + }, + To: types.AiringStatusDates{ + Day: anime.Data.Aired.Prop.To.Day, + Month: anime.Data.Aired.Prop.To.Month, + Year: anime.Data.Aired.Prop.To.Year, + String: anime.Data.Aired.To, + }, + String: anime.Data.Aired.String, + }, Duration: anime.Data.Duration, - Logos: logos, + Images: types.AnimeImages{ + Small: anime.Data.Images.JPG.SmallImageURL, + Large: anime.Data.Images.JPG.LargeImageURL, + Original: anime.Data.Images.JPG.ImageURL, + }, + Logos: logos, + Covers: types.AnimeImages{ + Small: anilistAnime.Data.Media.CoverImage.Medium, + Large: anilistAnime.Data.Media.CoverImage.Large, + Original: anilistAnime.Data.Media.CoverImage.ExtraLarge, + }, + Color: anilistAnime.Data.Media.CoverImage.Color, + Genres: generateGenres(anime.Data.Genres, anime.Data.ExplicitGenres), + Scores: types.AnimeScores{ + Score: anime.Data.Score, + ScoredBy: anime.Data.ScoredBy, + Rank: anime.Data.Rank, + Popularity: anime.Data.Popularity, + Members: anime.Data.Members, + Favorites: anime.Data.Favorites, + }, + Season: anime.Data.Season, + Year: anime.Data.Year, + Broadcast: types.AnimeBroadcast{ + Day: anime.Data.Broadcast.Day, + Time: anime.Data.Broadcast.Time, + Timezone: anime.Data.Broadcast.Timezone, + String: anime.Data.Broadcast.String, + }, + Producers: generateProducers(anime.Data.Producers), + Studios: generateStudios(anime.Data.Studios), + Licensors: generateLicensors(anime.Data.Licensors), + Seasons: seasons, // Add seasons information Episodes: types.AnimeEpisodes{ Total: getEpisodeCount(anime, anilistAnime), Aired: len(episodes.Data), @@ -90,3 +160,64 @@ func getEpisodeCount(malAnime *types.JikanAnimeResponse, anilistAnime *types.Ani return episodes } + +func generateGenres(genres, explicitGenres []types.JikanGenericMALStructure) []types.AnimeGenres { + animeGenres := make([]types.AnimeGenres, len(genres)+len(explicitGenres)) + counter := 0 + + for _, genre := range genres { + animeGenres[counter] = types.AnimeGenres{ + Name: genre.Name, + GenreID: genre.MALID, + URL: genre.URL, + } + counter++ + } + + for _, genre := range explicitGenres { + animeGenres[counter] = types.AnimeGenres{ + Name: genre.Name, + GenreID: genre.MALID, + URL: genre.URL, + } + counter++ + } + + return animeGenres +} + +func generateStudios(genericPLS []types.JikanGenericMALStructure) []types.AnimeStudio { + studios := make([]types.AnimeStudio, len(genericPLS)) + for i, studio := range genericPLS { + studios[i] = types.AnimeStudio{ + Name: studio.Name, + StudioID: studio.MALID, + URL: studio.URL, + } + } + return studios +} + +func generateProducers(genericPLS []types.JikanGenericMALStructure) []types.AnimeProducer { + producers := make([]types.AnimeProducer, len(genericPLS)) + for i, producer := range genericPLS { + producers[i] = types.AnimeProducer{ + Name: producer.Name, + ProducerID: producer.MALID, + URL: producer.URL, + } + } + return producers +} + +func generateLicensors(genericPLS []types.JikanGenericMALStructure) []types.AnimeLicensor { + licensors := make([]types.AnimeLicensor, len(genericPLS)) + for i, licensor := range genericPLS { + licensors[i] = types.AnimeLicensor{ + Name: licensor.Name, + ProducerID: licensor.MALID, + URL: licensor.URL, + } + } + return licensors +} diff --git a/utils/anime/jikan.go b/utils/anime/jikan.go index d51a3ca..c30a871 100644 --- a/utils/anime/jikan.go +++ b/utils/anime/jikan.go @@ -155,6 +155,121 @@ func waitForJikanRequest() { } func getAnimeViaJikan(malID int) (*types.JikanAnimeResponse, error) { + apiURL := fmt.Sprintf("https://api.jikan.moe/v4/anime/%d", malID) + maxRetries := 3 + baseBackoff := 1 * time.Second + + var animeResponse types.JikanAnimeResponse + success := false + retries := 0 + + for !success && retries <= maxRetries { + // Use rate limiter before making the request + logger.Log(fmt.Sprintf("Waiting for rate limiter before requesting anime %d details", malID), types.LogOptions{ + Level: types.Debug, + Prefix: "AnimeAPI", + }) + waitForJikanRequest() + + req, err := http.NewRequest("GET", apiURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + client := &http.Client{ + Timeout: 10 * time.Second, // Add timeout to prevent hanging requests + } + resp, err := client.Do(req) + + if err != nil { + if retries < maxRetries { + retries++ + backoffTime := time.Duration(float64(baseBackoff) * math.Pow(2, float64(retries-1))) + logger.Log(fmt.Sprintf("Request error for anime details, retrying in %v (retry %d/%d): %v", + backoffTime, retries, maxRetries, err), types.LogOptions{ + Level: types.Warn, + Prefix: "AnimeAPI", + }) + time.Sleep(backoffTime) + continue + } + return nil, fmt.Errorf("failed to execute request after %d retries: %w", maxRetries, err) + } + + defer resp.Body.Close() + + if resp.StatusCode == http.StatusTooManyRequests { + if retries < maxRetries { + retries++ + backoffTime := time.Duration(float64(baseBackoff) * math.Pow(2, float64(retries-1))) + logger.Log(fmt.Sprintf("Rate limited on anime details, backing off for %v (retry %d/%d)", + backoffTime, retries, maxRetries), types.LogOptions{ + Level: types.Warn, + Prefix: "AnimeAPI", + }) + time.Sleep(backoffTime) + continue + } + return nil, fmt.Errorf("failed to get anime data: rate limited after %d retries", maxRetries) + } else if resp.StatusCode != http.StatusOK { + if retries < maxRetries { + retries++ + backoffTime := time.Duration(float64(baseBackoff) * math.Pow(2, float64(retries-1))) + logger.Log(fmt.Sprintf("HTTP error %d for anime details, retrying in %v (retry %d/%d)", + resp.StatusCode, backoffTime, retries, maxRetries), types.LogOptions{ + Level: types.Warn, + Prefix: "AnimeAPI", + }) + time.Sleep(backoffTime) + continue + } + return nil, fmt.Errorf("failed to get anime data: %s", resp.Status) + } + // 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 < maxRetries { + retries++ + backoffTime := time.Duration(float64(baseBackoff) * math.Pow(2, float64(retries-1))) + logger.Log(fmt.Sprintf("Error reading response body, retrying in %v (retry %d/%d): %v", + backoffTime, retries, maxRetries, err), types.LogOptions{ + Level: types.Warn, + Prefix: "AnimeAPI", + }) + time.Sleep(backoffTime) + continue + } + return nil, fmt.Errorf("failed to read response body: %w", err) + } + if err := json.Unmarshal(bodyBytes, &animeResponse); err != nil { + if retries < maxRetries { + retries++ + backoffTime := time.Duration(float64(baseBackoff) * math.Pow(2, float64(retries-1))) + logger.Log(fmt.Sprintf("JSON decode error for anime details, retrying in %v (retry %d/%d): %v", + backoffTime, retries, maxRetries, err), types.LogOptions{ + Level: types.Warn, + Prefix: "AnimeAPI", + }) + time.Sleep(backoffTime) + continue + } + return nil, fmt.Errorf("failed to decode response: %w", err) + } + success = true + } + + if !success { + return nil, fmt.Errorf("failed to fetch anime details after maximum retries") + } + + if animeResponse.Data.MALID == 0 { + return nil, fmt.Errorf("no data found for MAL ID %d", malID) + } + + return &animeResponse, nil +} + +func getFullAnimeViaJikan(malID int) (*types.JikanAnimeResponse, error) { apiURL := fmt.Sprintf("https://api.jikan.moe/v4/anime/%d/full", malID) maxRetries := 3 baseBackoff := 1 * time.Second diff --git a/utils/anime/tvdb.go b/utils/anime/tvdb.go new file mode 100644 index 0000000..4206c24 --- /dev/null +++ b/utils/anime/tvdb.go @@ -0,0 +1,163 @@ +package anime + +import ( + "fmt" + "metachan/database" + "metachan/entities" + "metachan/types" + "metachan/utils/logger" + "sort" + "strings" +) + +// GetAnimeSeason prepares season information for an anime +func GetAnimeSeason(mappings *[]entities.AnimeMapping, currentMALID int) ([]types.AnimeSeason, error) { + var seasons []types.AnimeSeason + + for _, mapping := range *mappings { + // Fetch basic anime info from Jikan API + animeDetails, err := getAnimeViaJikan(mapping.MAL) + if err != nil { + logger.Log(fmt.Sprintf("Failed to get anime details for MAL ID %d: %v", mapping.MAL, err), types.LogOptions{ + Level: types.Warn, + Prefix: "AnimeSeason", + }) + continue + } + + // Build the season object + season := types.AnimeSeason{ + MALID: mapping.MAL, + Titles: types.AnimeTitles{ + English: animeDetails.Data.TitleEnglish, + Japanese: animeDetails.Data.TitleJapanese, + Romaji: animeDetails.Data.Title, + Synonyms: animeDetails.Data.TitleSynonyms, + }, + Synopsis: animeDetails.Data.Synopsis, + Type: types.AniSyncType(mapping.Type), + Source: animeDetails.Data.Source, + Airing: animeDetails.Data.Airing, + Status: animeDetails.Data.Status, + AiringStatus: types.AiringStatus{ + From: types.AiringStatusDates{ + Day: animeDetails.Data.Aired.Prop.From.Day, + Month: animeDetails.Data.Aired.Prop.From.Month, + Year: animeDetails.Data.Aired.Prop.From.Year, + String: animeDetails.Data.Aired.From, + }, + To: types.AiringStatusDates{ + Day: animeDetails.Data.Aired.Prop.To.Day, + Month: animeDetails.Data.Aired.Prop.To.Month, + Year: animeDetails.Data.Aired.Prop.To.Year, + String: animeDetails.Data.Aired.To, + }, + String: animeDetails.Data.Aired.String, + }, + Duration: animeDetails.Data.Duration, + Images: types.AnimeImages{ + Small: animeDetails.Data.Images.JPG.SmallImageURL, + Large: animeDetails.Data.Images.JPG.LargeImageURL, + Original: animeDetails.Data.Images.JPG.ImageURL, + }, + Scores: types.AnimeScores{ + Score: animeDetails.Data.Score, + ScoredBy: animeDetails.Data.ScoredBy, + Rank: animeDetails.Data.Rank, + Popularity: animeDetails.Data.Popularity, + Members: animeDetails.Data.Members, + Favorites: animeDetails.Data.Favorites, + }, + Season: animeDetails.Data.Season, + Year: animeDetails.Data.Year, + Current: mapping.MAL == currentMALID, // Mark if this is the current season + } + + seasons = append(seasons, season) + } + + // Sort seasons chronologically by air date + if len(seasons) > 1 { + sortSeasonsByAirDate(&seasons) + logger.Log(fmt.Sprintf("Found and sorted %d seasons for anime", len(seasons)), types.LogOptions{ + Level: types.Info, + Prefix: "AnimeSeason", + }) + } + + return seasons, nil +} + +// sortSeasonsByAirDate sorts the seasons array chronologically by air date +func sortSeasonsByAirDate(seasons *[]types.AnimeSeason) { + // Use a slice instead of a pointer to slice to make the code cleaner + s := *seasons + + // Sort by air date using the structured fields (year, month, day) + sort.Slice(s, func(i, j int) bool { + // Compare years first + if s[i].AiringStatus.From.Year != s[j].AiringStatus.From.Year { + return s[i].AiringStatus.From.Year < s[j].AiringStatus.From.Year + } + + // If years are equal, compare months + if s[i].AiringStatus.From.Month != s[j].AiringStatus.From.Month { + return s[i].AiringStatus.From.Month < s[j].AiringStatus.From.Month + } + + // If months are equal, compare days + if s[i].AiringStatus.From.Day != s[j].AiringStatus.From.Day { + return s[i].AiringStatus.From.Day < s[j].AiringStatus.From.Day + } + + // If all date fields are equal, use season as a tiebreaker + if s[i].Season != s[j].Season { + seasonOrder := map[string]int{ + "winter": 0, + "spring": 1, + "summer": 2, + "fall": 3, + "": 4, // Unknown season comes last + } + + seasonI := strings.ToLower(s[i].Season) + seasonJ := strings.ToLower(s[j].Season) + + return seasonOrder[seasonI] < seasonOrder[seasonJ] + } + + // If everything is equal, preserve original order (stable sort) + return i < j + }) + + // Update the original slice + *seasons = s +} + +// FindSeasonMappings finds all anime mappings that belong to the same series based on TVDB ID +func FindSeasonMappings(tvdbID int) ([]entities.AnimeMapping, error) { + logger.Log(fmt.Sprintf("Finding season mappings for TVDB ID %d", tvdbID), types.LogOptions{ + Level: types.Debug, + Prefix: "TVDB", + }) + + // Use our database function to find all mappings with the same TVDB ID + mappings, err := database.GetAnimeMappingsByTVDBID(tvdbID) + if err != nil { + return nil, fmt.Errorf("failed to get season mappings: %w", err) + } + + if len(mappings) == 0 { + logger.Log(fmt.Sprintf("No season mappings found for TVDB ID %d", tvdbID), types.LogOptions{ + Level: types.Debug, + Prefix: "TVDB", + }) + } else { + logger.Log(fmt.Sprintf("Found %d season mappings for TVDB ID %d", len(mappings), tvdbID), types.LogOptions{ + Level: types.Info, + Prefix: "TVDB", + }) + } + + return mappings, nil +} |
