package jikan import ( "fmt" "strconv" "strings" ) type SearchResult struct { Animes []Anime HasNextPage bool } type TopAnimeResult struct { Animes []Anime HasNextPage bool } type StudioAnimeResult struct { Animes []Anime HasNextPage bool StudioName string } type NamedEntity struct { MalID int `json:"mal_id"` Name string `json:"name"` } type Aired struct { From string `json:"from"` To string `json:"to"` String string `json:"string"` } type TitleEntry struct { Type string `json:"type"` Title string `json:"title"` } type Anime struct { MalID int `json:"mal_id"` Title string `json:"title"` TitleEnglish string `json:"title_english"` TitleJapanese string `json:"title_japanese"` TitleSynonyms []string `json:"title_synonyms"` Titles []TitleEntry `json:"titles"` Images struct { Jpg struct { LargeImageURL string `json:"large_image_url"` } `json:"jpg"` Webp struct { LargeImageURL string `json:"large_image_url"` } `json:"webp"` } `json:"images"` Synopsis string `json:"synopsis"` Rank int `json:"rank"` Popularity int `json:"popularity"` Status string `json:"status"` Airing bool `json:"airing"` Episodes int `json:"episodes"` Score float64 `json:"score"` Season string `json:"season"` Year int `json:"year"` Type string `json:"type"` Rating string `json:"rating"` Duration string `json:"duration"` ScoredBy int `json:"scored_by"` Aired Aired `json:"aired"` Genres []NamedEntity `json:"genres"` Studios []NamedEntity `json:"studios"` Producers []NamedEntity `json:"producers"` Themes []NamedEntity `json:"themes"` Source string `json:"source"` Background string `json:"background"` Favorites int `json:"favorites"` Members int `json:"members"` Demographics []NamedEntity `json:"demographics"` Licensors []NamedEntity `json:"licensors"` Broadcast struct { Day string `json:"day"` Time string `json:"time"` Timezone string `json:"timezone"` String string `json:"string"` } `json:"broadcast"` 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"` Streaming []struct { Name string `json:"name"` URL string `json:"url"` } `json:"streaming"` Relations []JikanRelationGroup `json:"relations"` External []struct { Name string `json:"name"` URL string `json:"url"` } `json:"external"` } type CharacterVoiceActor struct { Person struct { MalID int `json:"mal_id"` URL string `json:"url"` Images struct { Jpg struct { ImageURL string `json:"image_url"` } `json:"jpg"` } `json:"images"` Name string `json:"name"` } `json:"person"` Language string `json:"language"` } type CharacterEntry struct { Character 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"` SmallImageURL string `json:"small_image_url"` } `json:"webp"` } `json:"images"` Name string `json:"name"` } `json:"character"` Role string `json:"role"` VoiceActors []CharacterVoiceActor `json:"voice_actors"` } type CharactersResponse struct { Data []CharacterEntry `json:"data"` } type RecommendationEntry struct { Entry struct { MalID int `json:"mal_id"` URL string `json:"url"` Images struct { Webp struct { LargeImageURL string `json:"large_image_url"` } `json:"webp"` } `json:"images"` Title string `json:"title"` } `json:"entry"` URL string `json:"url"` Votes int `json:"votes"` } type RecommendationsResponse struct { Data []RecommendationEntry `json:"data"` } // ScoredByFormatted returns formatted count (e.g. "1 234 567"). func (a Anime) ScoredByFormatted() string { return formatNumber(a.ScoredBy) } // MembersFormatted returns formatted count (e.g. "1 234 567"). func (a Anime) MembersFormatted() string { return formatNumber(a.Members) } // FavoritesFormatted returns formatted count (e.g. "1 234 567"). func (a Anime) FavoritesFormatted() string { return formatNumber(a.Favorites) } // formatNumber adds space separators to a number (1234567 -> "1 234 567"). func formatNumber(n int) string { if n == 0 { return "" } s := fmt.Sprintf("%d", n) var res []string for i := len(s); i > 0; i -= 3 { start := max(i-3, 0) res = append([]string{s[start:i]}, res...) } return strings.Join(res, " ") } // ImageURL returns the webp large image URL for the anime. func (a Anime) ImageURL() string { return a.Images.Webp.LargeImageURL } // ShortRating extracts just the rating code (e.g. "PG-13") from full rating string. func (a Anime) ShortRating() string { if a.Rating == "" { return "" } // Rating format: "PG-13 - Teens 13 or older" for i, c := range a.Rating { if c == ' ' && i > 0 { return a.Rating[:i] } } return a.Rating } // ShortDuration extracts numeric duration in minutes (e.g. "23m" from "23 min per ep"). func (a Anime) ShortDuration() string { if a.Duration == "" { return "" } // Duration format: "23 min per ep" or "1 hr 30 min" var num strings.Builder for _, c := range a.Duration { if c >= '0' && c <= '9' { num.WriteString(string(c)) } else if c == ' ' && num.String() != "" { break } } if num.String() != "" { return num.String() + "m" } return a.Duration } // DurationSeconds converts duration string to total seconds (e.g. "1 hr 30 min" -> 5400). func (a Anime) DurationSeconds() float64 { if a.Duration == "" { return 0 } var hours, minutes int var currentValue int hasValue := false for _, token := range strings.Fields(strings.ToLower(a.Duration)) { value, err := strconv.Atoi(token) if err == nil { currentValue = value hasValue = true continue } if !hasValue { continue } switch { case strings.HasPrefix(token, "h"): hours = currentValue hasValue = false case strings.HasPrefix(token, "m"): minutes = currentValue hasValue = false } } if hasValue { minutes = currentValue } return float64(hours*60+minutes) * 60 } // Premiered returns formatted premiere string (e.g. "Winter 2020"). func (a Anime) Premiered() string { if a.Season != "" && a.Year > 0 { return fmt.Sprintf("%s %d", seasonLabel(a.Season), a.Year) } return "" } // seasonLabel normalizes season string to title case (fall/autumn -> Fall). func seasonLabel(season string) string { switch strings.ToLower(season) { case "winter": return "Winter" case "spring": return "Spring" case "summer": return "Summer" case "fall", "autumn": return "Fall" default: if season == "" { return "" } return strings.ToUpper(season[:1]) + strings.ToLower(season[1:]) } } type AnimeResponse struct { Data Anime `json:"data"` } type Genre struct { MalID int `json:"mal_id"` Name string `json:"name"` } type GenresResponse struct { Data []Genre `json:"data"` } type SearchResponse struct { Data []Anime `json:"data"` Pagination Pagination `json:"pagination"` } type Pagination struct { LastVisiblePage int `json:"last_visible_page"` HasNextPage bool `json:"has_next_page"` } type TopAnimeResponse struct { Data []Anime `json:"data"` Pagination Pagination `json:"pagination"` } type EpisodeImages struct { Jpg struct { ImageURL string `json:"image_url"` } `json:"jpg"` } type Episode struct { MalID int `json:"mal_id"` Title string `json:"title"` Episode string `json:"episode"` Filler bool `json:"filler"` Recap bool `json:"recap"` Images *EpisodeImages `json:"images,omitempty"` } type EpisodesResponse struct { Data []Episode `json:"data"` Pagination Pagination `json:"pagination"` } type EpisodeResponse struct { Data Episode `json:"data"` } type JikanRelationEntry struct { MalID int `json:"mal_id"` Type string `json:"type"` Name string `json:"name"` URL string `json:"url"` } type JikanRelationGroup struct { Relation string `json:"relation"` Entry []JikanRelationEntry `json:"entry"` } type JikanRelationsResponse struct { Data []JikanRelationGroup `json:"data"` } type RelationEntry struct { Anime Anime Relation string IsCurrent bool IsExtra bool } // Staff types type StaffEntry struct { Person struct { MalID int `json:"mal_id"` URL string `json:"url"` Images struct { Jpg struct { ImageURL string `json:"image_url"` } `json:"jpg"` } `json:"images"` Name string `json:"name"` } `json:"person"` Positions []string `json:"positions"` } type StaffResponse struct { Data []StaffEntry `json:"data"` } type StatisticsScore struct { Score int `json:"score"` Votes int `json:"votes"` Percentage float64 `json:"percentage"` } type Statistics struct { Watching int `json:"watching"` Completed int `json:"completed"` OnHold int `json:"on_hold"` Dropped int `json:"dropped"` PlanToWatch int `json:"plan_to_watch"` Total int `json:"total"` Scores []StatisticsScore `json:"scores"` } type StatisticsResponse struct { Data Statistics `json:"data"` } type ThemesData struct { Openings []string `json:"openings"` Endings []string `json:"endings"` } type ThemesResponse struct { Data ThemesData `json:"data"` } type ReviewReactions struct { Overall int `json:"overall"` Nice int `json:"nice"` LoveIt int `json:"love_it"` Funny int `json:"funny"` Confusing int `json:"confusing"` Informative int `json:"informative"` WellWritten int `json:"well_written"` Creative int `json:"creative"` } type ReviewEntry struct { MalID int `json:"mal_id"` URL string `json:"url"` Type string `json:"type"` Reactions ReviewReactions `json:"reactions"` Date string `json:"date"` Review string `json:"review"` Score int `json:"score"` Tags []string `json:"tags"` IsSpoiler bool `json:"is_spoiler"` IsPreliminary bool `json:"is_preliminary"` EpisodesSeen int `json:"episodes_seen"` User struct { URL string `json:"url"` Username string `json:"username"` Images struct { Jpg struct { ImageURL string `json:"image_url"` } `json:"jpg"` Webp struct { ImageURL string `json:"image_url"` } `json:"webp"` } `json:"images"` } `json:"user"` } type ReviewsResponse struct { Data []ReviewEntry `json:"data"` Pagination Pagination `json:"pagination"` } // DisplayTitle returns English title if available, otherwise default title, titles[0], then Japanese. func (a Anime) DisplayTitle() string { if a.TitleEnglish != "" { return a.TitleEnglish } if a.Title != "" { return a.Title } if len(a.Titles) > 0 && a.Titles[0].Title != "" { return a.Titles[0].Title } return a.TitleJapanese }