aboutsummaryrefslogtreecommitdiff
path: root/services/anime/helpers.go
diff options
context:
space:
mode:
Diffstat (limited to 'services/anime/helpers.go')
-rw-r--r--services/anime/helpers.go1108
1 files changed, 554 insertions, 554 deletions
diff --git a/services/anime/helpers.go b/services/anime/helpers.go
index c8bf51d..061ffae 100644
--- a/services/anime/helpers.go
+++ b/services/anime/helpers.go
@@ -1,556 +1,556 @@
package anime
-import (
- "crypto/md5"
- "crypto/tls"
- "fmt"
- "metachan/types"
- "metachan/utils/api/anilist"
- "metachan/utils/api/jikan"
- "metachan/utils/api/malsync"
- "metachan/utils/api/tmdb"
- "metachan/utils/api/tvdb"
- "metachan/utils/logger"
- "net/http"
- "strings"
- "time"
-)
-
-func generateEpisodeID(malID int, episodeNumber int, titles types.EpisodeTitles) string {
- var title string
- if titles.English != "" {
- title = titles.English
- } else if titles.Romaji != "" {
- title = titles.Romaji
- } else {
- title = titles.Japanese
- }
-
- // Include MAL ID and episode number to ensure uniqueness across all anime
- uniqueString := fmt.Sprintf("%d-%d-%s", malID, episodeNumber, title)
- hash := md5.Sum([]byte(uniqueString))
- return fmt.Sprintf("%x", hash)
-}
-
-func generateBasicEpisodes(malID int, episodes []jikan.JikanAnimeEpisode) []types.AnimeSingleEpisode {
- var animeEpisodes []types.AnimeSingleEpisode
-
- for _, episode := range episodes {
- titles := types.EpisodeTitles{
- English: episode.Title,
- Japanese: episode.TitleJapanese,
- Romaji: episode.TitleRomaji,
- }
-
- animeEpisodes = append(animeEpisodes, types.AnimeSingleEpisode{
- ID: generateEpisodeID(malID, episode.MALID, titles),
- Titles: titles,
- Aired: episode.Aired,
- Score: episode.Score,
- Filler: episode.Filler,
- Recap: episode.Recap,
- ForumURL: episode.ForumURL,
- URL: episode.URL,
- Description: "No description available",
- ThumbnailURL: "",
- })
- }
- return animeEpisodes
-}
-
-// getEpisodeCount determines the highest episode count from different sources
-func getEpisodeCount(malAnime *jikan.JikanAnimeResponse, anilistAnime *anilist.AnilistAnimeResponse) int {
- if anilistAnime == nil {
- return malAnime.Data.Episodes
- }
-
- streamingScheduleLength := len(anilistAnime.Data.Media.AiringSchedule.Nodes)
- episodes := max(malAnime.Data.Episodes, anilistAnime.Data.Media.Episodes)
- episodes = max(episodes, streamingScheduleLength)
-
- return episodes
-}
-
-// getEpisodeCountWithAiredFallback determines the total episode count, using aired episodes as fallback for long-running series
-func getEpisodeCountWithAiredFallback(malAnime *jikan.JikanAnimeResponse, anilistAnime *anilist.AnilistAnimeResponse, airedCount int) int {
- totalFromAPIs := getEpisodeCount(malAnime, anilistAnime)
-
- // For long-running series, if the aired count is significantly higher than API-reported total,
- // use the aired count as a more accurate total (since APIs often report season/arc counts)
- if airedCount > totalFromAPIs && airedCount > 100 {
- // This indicates a long-running series where APIs might be reporting seasonal data
- // For ongoing series, total should be at least as high as aired episodes
- return airedCount
- }
-
- // For normal series, use the maximum from APIs
- return max(totalFromAPIs, airedCount)
-}
-
-// sortSeasonsByAirDate sorts the seasons array chronologically by air date
-func sortSeasonsByAirDate(seasons *[]types.AnimeSeason) {
- // First, collect seasons with valid dates
- seasonsWithDates := make([]types.AnimeSeason, 0)
- seasonsWithoutDates := make([]types.AnimeSeason, 0)
-
- for _, season := range *seasons {
- if season.AiringStatus.From.Year > 0 {
- seasonsWithDates = append(seasonsWithDates, season)
- } else {
- seasonsWithoutDates = append(seasonsWithoutDates, season)
- }
- }
-
- // Sort seasons with dates
- if len(seasonsWithDates) > 0 {
- sortedSeasons := make([]types.AnimeSeason, len(seasonsWithDates))
- copy(sortedSeasons, seasonsWithDates)
-
- for i := 0; i < len(sortedSeasons)-1; i++ {
- for j := i + 1; j < len(sortedSeasons); j++ {
- a := sortedSeasons[i].AiringStatus.From
- b := sortedSeasons[j].AiringStatus.From
-
- // Compare years
- if a.Year > b.Year {
- sortedSeasons[i], sortedSeasons[j] = sortedSeasons[j], sortedSeasons[i]
- } else if a.Year == b.Year {
- // Compare months if years are equal
- if a.Month > b.Month {
- sortedSeasons[i], sortedSeasons[j] = sortedSeasons[j], sortedSeasons[i]
- } else if a.Month == b.Month {
- // Compare days if months are equal
- if a.Day > b.Day {
- sortedSeasons[i], sortedSeasons[j] = sortedSeasons[j], sortedSeasons[i]
- }
- }
- }
- }
- }
-
- // Combine sorted dates with no-date seasons
- result := append(sortedSeasons, seasonsWithoutDates...)
- *seasons = result
- }
-}
-
-// generateGenres converts Jikan genre structures to our format
-func generateGenres(genres, explicitGenres []jikan.JikanGenericMALStructure) []types.AnimeGenres {
- var animeGenres []types.AnimeGenres
-
- // Add regular genres
- for _, genre := range genres {
- animeGenres = append(animeGenres, types.AnimeGenres{
- Name: genre.Name,
- GenreID: genre.MALID,
- URL: genre.URL,
- })
- }
-
- // Add explicit genres if any
- for _, genre := range explicitGenres {
- animeGenres = append(animeGenres, types.AnimeGenres{
- Name: genre.Name,
- GenreID: genre.MALID,
- URL: genre.URL,
- })
- }
-
- return animeGenres
-}
-
-// generateStudios converts Jikan studio structures to our format
-func generateStudios(studios []jikan.JikanGenericMALStructure) []types.AnimeProducer {
- var animeStudios []types.AnimeProducer
-
- for _, studio := range studios {
- animeStudios = append(animeStudios, types.AnimeProducer{
- Name: studio.Name,
- MALID: studio.MALID,
- URL: studio.URL,
- })
- }
-
- return animeStudios
-}
-
-// generateProducers converts Jikan producer structures to our format
-func generateProducers(producers []jikan.JikanGenericMALStructure) []types.AnimeProducer {
- var animeProducers []types.AnimeProducer
-
- for _, producer := range producers {
- animeProducers = append(animeProducers, types.AnimeProducer{
- Name: producer.Name,
- MALID: producer.MALID,
- URL: producer.URL,
- })
- }
-
- return animeProducers
-}
-
-// generateLicensors converts Jikan licensor structures to our format
-func generateLicensors(licensors []jikan.JikanGenericMALStructure) []types.AnimeProducer {
- var animeLicensors []types.AnimeProducer
-
- for _, licensor := range licensors {
- animeLicensors = append(animeLicensors, types.AnimeProducer{
- Name: licensor.Name,
- MALID: licensor.MALID,
- URL: licensor.URL,
- })
- }
-
- return animeLicensors
-}
-
-// getAnimeCharacters processes character data from Jikan
-func getAnimeCharacters(characterResponse *jikan.JikanAnimeCharacterResponse) []types.AnimeCharacter {
- var characters []types.AnimeCharacter
-
- for _, entry := range characterResponse.Data {
- character := types.AnimeCharacter{
- MALID: entry.Character.MALID,
- URL: entry.Character.URL,
- ImageURL: entry.Character.Images.JPG.ImageURL,
- Name: entry.Character.Name,
- Role: entry.Role,
- }
-
- for _, va := range entry.VoiceActors {
- character.VoiceActors = append(character.VoiceActors, types.AnimeVoiceActor{
- MALID: va.Person.MALID,
- URL: va.Person.URL,
- Image: va.Person.Images.JPG.ImageURL,
- Name: va.Person.Name,
- Language: va.Language,
- })
- }
-
- characters = append(characters, character)
- }
-
- return characters
-}
-
-// getNextAiringEpisode extracts next airing episode data from AniList
-func getNextAiringEpisode(anilistAnime *anilist.AnilistAnimeResponse) types.AnimeAiringEpisode {
- if anilistAnime == nil || anilistAnime.Data.Media.ID == 0 {
- return types.AnimeAiringEpisode{}
- }
-
- // Get the current time to determine the next episode
- currentTime := time.Now().Unix()
- nextEpisode := anilistAnime.Data.Media.NextAiringEpisode
-
- // If AniList provides a valid next airing episode directly, use it
- if nextEpisode.AiringAt > 0 && nextEpisode.Episode > 0 {
- return types.AnimeAiringEpisode{
- AiringAt: nextEpisode.AiringAt,
- Episode: nextEpisode.Episode,
- }
- }
-
- // If AniList doesn't provide a direct next episode, but we have airing schedule nodes
- // Find the next episode that hasn't aired yet
- if len(anilistAnime.Data.Media.AiringSchedule.Nodes) > 0 {
- var nextAiringEpisode types.AnimeAiringEpisode
-
- for _, node := range anilistAnime.Data.Media.AiringSchedule.Nodes {
- if int64(node.AiringAt) > currentTime {
- // If this is the first future episode we've found, or it airs sooner than our current "next"
- if nextAiringEpisode.AiringAt == 0 || node.AiringAt < nextAiringEpisode.AiringAt {
- nextAiringEpisode.AiringAt = node.AiringAt
- nextAiringEpisode.Episode = node.Episode
- }
- }
- }
-
- // If we found a next episode
- if nextAiringEpisode.AiringAt > 0 {
- return nextAiringEpisode
- }
- }
-
- return types.AnimeAiringEpisode{}
-}
-
-// getAnimeSchedule extracts airing schedule data from AniList
-func getAnimeSchedule(anilistAnime *anilist.AnilistAnimeResponse) []types.AnimeAiringEpisode {
- if anilistAnime == nil || anilistAnime.Data.Media.AiringSchedule.Nodes == nil {
- return []types.AnimeAiringEpisode{}
- }
-
- var schedule []types.AnimeAiringEpisode
-
- for _, node := range anilistAnime.Data.Media.AiringSchedule.Nodes {
- schedule = append(schedule, types.AnimeAiringEpisode{
- AiringAt: node.AiringAt,
- Episode: node.Episode,
- })
- }
-
- return schedule
-}
-
-// AttachEpisodeDescriptions enhances episode information with external data
-// Imports the function from the anime utils to use in our service
-var AttachEpisodeDescriptions = tmdb.AttachEpisodeDescriptions
-
-// extractLogosFromMALSync extracts logo images from MALSync data
-func extractLogosFromMALSync(malSyncData *malsync.MALSyncAnimeResponse) types.AnimeLogos {
- logos := types.AnimeLogos{}
-
- // Early return if no data
- if malSyncData == nil {
- return logos
- }
-
- // Check if Crunchyroll data exists in the MALSync response
- crunchyrollSites, exists := malSyncData.Sites["Crunchyroll"]
- if !exists || len(crunchyrollSites) == 0 {
- logger.Log("No Crunchyroll data found in MALSync response", logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
- return logos
- }
-
- // Get the Crunchyroll URL from any of the entries
- crURL := ""
- for _, site := range crunchyrollSites {
- crURL = site.URL
- break // Take the first URL
- }
-
- if crURL == "" {
- logger.Log("No valid Crunchyroll URL found", logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
- return logos
- }
-
- // Extract series ID from URL
- seriesID := extractCrunchyrollSeriesID(crURL)
- if seriesID == "" {
- return logos
- }
-
- // Define logo sizes
- logoSizes := map[string]int{
- "Small": 320,
- "Medium": 480,
- "Large": 600,
- "XLarge": 800,
- "Original": 1200,
- }
-
- // Generate logo URLs
- logos.Small = fmt.Sprintf("https://imgsrv.crunchyroll.com/cdn-cgi/image/fit=contain,format=auto,quality=85,width=%d/keyart/%s-title_logo-en-us", logoSizes["Small"], seriesID)
- logos.Medium = fmt.Sprintf("https://imgsrv.crunchyroll.com/cdn-cgi/image/fit=contain,format=auto,quality=85,width=%d/keyart/%s-title_logo-en-us", logoSizes["Medium"], seriesID)
- logos.Large = fmt.Sprintf("https://imgsrv.crunchyroll.com/cdn-cgi/image/fit=contain,format=auto,quality=85,width=%d/keyart/%s-title_logo-en-us", logoSizes["Large"], seriesID)
- logos.XLarge = fmt.Sprintf("https://imgsrv.crunchyroll.com/cdn-cgi/image/fit=contain,format=auto,quality=85,width=%d/keyart/%s-title_logo-en-us", logoSizes["XLarge"], seriesID)
- logos.Original = fmt.Sprintf("https://imgsrv.crunchyroll.com/cdn-cgi/image/fit=contain,format=auto,quality=85,width=%d/keyart/%s-title_logo-en-us", logoSizes["Original"], seriesID)
-
- logger.Log(fmt.Sprintf("Successfully generated logo URLs for series ID: %s", seriesID), logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
-
- return logos
-}
-
-// extractCrunchyrollSeriesID extracts the series ID from a Crunchyroll URL
-func extractCrunchyrollSeriesID(crURL string) string {
- logger.Log(fmt.Sprintf("Attempting to extract series ID from URL: %s", crURL), logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
-
- // Direct series URL format
- if strings.Contains(crURL, "/series/") {
- parts := strings.Split(crURL, "/series/")
- if len(parts) < 2 {
- logger.Log("URL contains /series/ but couldn't extract ID part", logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
- return ""
- }
-
- idParts := strings.Split(parts[1], "/")
- if len(idParts) < 1 {
- logger.Log("Couldn't extract ID from path segments", logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
- return ""
- }
-
- logger.Log(fmt.Sprintf("Found series ID directly in URL: %s", idParts[0]), logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
- return idParts[0]
- }
-
- // Need to follow redirect to get series ID
- logger.Log("URL doesn't contain /series/, following redirect...", logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
-
- // Create a transport that uses modern TLS settings
- transport := &http.Transport{
- TLSClientConfig: &tls.Config{
- MinVersion: tls.VersionTLS12,
- },
- ForceAttemptHTTP2: true,
- }
-
- client := &http.Client{
- CheckRedirect: func(req *http.Request, via []*http.Request) error {
- // Don't follow redirects, just capture the Location header
- return http.ErrUseLastResponse
- },
- Timeout: 10 * time.Second,
- Transport: transport,
- }
-
- // Update HTTP to HTTPS for Crunchyroll URLs if needed
- if strings.HasPrefix(crURL, "http://www.crunchyroll.com") {
- crURL = strings.Replace(crURL, "http://", "https://", 1)
- logger.Log(fmt.Sprintf("Updated URL to HTTPS: %s", crURL), logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
- }
-
- // Add User-Agent header to mimic a browser
- req, err := http.NewRequest("GET", crURL, nil)
- if err != nil {
- logger.Log(fmt.Sprintf("Failed to create request for Crunchyroll URL: %v", err), logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
- return ""
- }
-
- 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")
- req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml")
-
- resp, err := client.Do(req)
- if err != nil {
- logger.Log(fmt.Sprintf("Failed to get Crunchyroll redirect: %v", err), logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
- return ""
- }
- defer resp.Body.Close()
-
- // Log the status code and response headers for debugging
- logger.Log(fmt.Sprintf("Crunchyroll response status: %d %s", resp.StatusCode, resp.Status), logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
-
- for name, values := range resp.Header {
- logger.Log(fmt.Sprintf("Header %s: %s", name, strings.Join(values, ", ")), logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
- }
-
- // Check for specific status codes for redirects
- if resp.StatusCode != http.StatusMovedPermanently &&
- resp.StatusCode != http.StatusFound &&
- resp.StatusCode != http.StatusTemporaryRedirect &&
- resp.StatusCode != http.StatusPermanentRedirect {
- logger.Log(fmt.Sprintf("Unexpected status code from Crunchyroll: %d", resp.StatusCode), logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
-
- // If we got a 200 OK, maybe Crunchyroll served the page directly
- // Try to extract the series ID from the URL itself as a fallback
- if resp.StatusCode == http.StatusOK && strings.Contains(crURL, "crunchyroll.com") {
- // For URLs like http://www.crunchyroll.com/fullmetal-alchemist-brotherhood
- // Extract the last part as a potential identifier
- urlParts := strings.Split(crURL, "/")
- if len(urlParts) > 0 {
- potentialId := urlParts[len(urlParts)-1]
- logger.Log(fmt.Sprintf("Extracted potential series ID from original URL: %s", potentialId), logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
- return potentialId
- }
- }
- return ""
- }
-
- redirectURL := resp.Header.Get("Location")
- if redirectURL == "" {
- logger.Log("No redirect URL found in Crunchyroll response", logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
- return ""
- }
-
- logger.Log(fmt.Sprintf("Found redirect URL: %s", redirectURL), logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
-
- // Extract series ID from redirect URL
- if strings.Contains(redirectURL, "/series/") {
- parts := strings.Split(redirectURL, "/series/")
- if len(parts) < 2 {
- return ""
- }
-
- idParts := strings.Split(parts[1], "/")
- if len(idParts) < 1 {
- return ""
- }
-
- logger.Log(fmt.Sprintf("Successfully extracted series ID from redirect: %s", idParts[0]), logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
- return idParts[0]
- }
-
- // For multi-level redirects, try to follow one more time
- if strings.Contains(redirectURL, "crunchyroll.com") {
- logger.Log("Trying to follow one more redirect level...", logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
- return extractCrunchyrollSeriesID(redirectURL)
- }
-
- // As a fallback for older Crunchyroll URLs like fullmetal-alchemist-brotherhood
- // Use the last path segment as the ID
- urlParts := strings.Split(crURL, "/")
- if len(urlParts) > 0 {
- potentialId := urlParts[len(urlParts)-1]
- logger.Log(fmt.Sprintf("Using fallback: extracted ID from original URL: %s", potentialId), logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
- return potentialId
- }
-
- logger.Log("Could not extract series ID from Crunchyroll redirect URL", logger.LogOptions{
- Level: logger.Debug,
- Prefix: "AnimeAPI",
- })
- return ""
-}
-
-// FindSeasonMappings finds all anime mappings that belong to the same series based on TVDB ID
-var FindSeasonMappings = tvdb.FindSeasonMappings
+// import (
+// "crypto/md5"
+// "crypto/tls"
+// "fmt"
+// "metachan/types"
+// "metachan/utils/api/anilist"
+// "metachan/utils/api/jikan"
+// "metachan/utils/api/malsync"
+// "metachan/utils/api/tmdb"
+// "metachan/utils/api/tvdb"
+// "metachan/utils/logger"
+// "net/http"
+// "strings"
+// "time"
+// )
+
+// func generateEpisodeID(malID int, episodeNumber int, titles types.EpisodeTitles) string {
+// var title string
+// if titles.English != "" {
+// title = titles.English
+// } else if titles.Romaji != "" {
+// title = titles.Romaji
+// } else {
+// title = titles.Japanese
+// }
+
+// // Include MAL ID and episode number to ensure uniqueness across all anime
+// uniqueString := fmt.Sprintf("%d-%d-%s", malID, episodeNumber, title)
+// hash := md5.Sum([]byte(uniqueString))
+// return fmt.Sprintf("%x", hash)
+// }
+
+// func generateBasicEpisodes(malID int, episodes []jikan.JikanAnimeEpisode) []types.AnimeSingleEpisode {
+// var animeEpisodes []types.AnimeSingleEpisode
+
+// for _, episode := range episodes {
+// titles := types.EpisodeTitles{
+// English: episode.Title,
+// Japanese: episode.TitleJapanese,
+// Romaji: episode.TitleRomaji,
+// }
+
+// animeEpisodes = append(animeEpisodes, types.AnimeSingleEpisode{
+// ID: generateEpisodeID(malID, episode.MALID, titles),
+// Titles: titles,
+// Aired: episode.Aired,
+// Score: episode.Score,
+// Filler: episode.Filler,
+// Recap: episode.Recap,
+// ForumURL: episode.ForumURL,
+// URL: episode.URL,
+// Description: "No description available",
+// ThumbnailURL: "",
+// })
+// }
+// return animeEpisodes
+// }
+
+// // getEpisodeCount determines the highest episode count from different sources
+// func getEpisodeCount(malAnime *jikan.JikanAnimeResponse, anilistAnime *anilist.AnilistAnimeResponse) int {
+// if anilistAnime == nil {
+// return malAnime.Data.Episodes
+// }
+
+// streamingScheduleLength := len(anilistAnime.Data.Media.AiringSchedule.Nodes)
+// episodes := max(malAnime.Data.Episodes, anilistAnime.Data.Media.Episodes)
+// episodes = max(episodes, streamingScheduleLength)
+
+// return episodes
+// }
+
+// // getEpisodeCountWithAiredFallback determines the total episode count, using aired episodes as fallback for long-running series
+// func getEpisodeCountWithAiredFallback(malAnime *jikan.JikanAnimeResponse, anilistAnime *anilist.AnilistAnimeResponse, airedCount int) int {
+// totalFromAPIs := getEpisodeCount(malAnime, anilistAnime)
+
+// // For long-running series, if the aired count is significantly higher than API-reported total,
+// // use the aired count as a more accurate total (since APIs often report season/arc counts)
+// if airedCount > totalFromAPIs && airedCount > 100 {
+// // This indicates a long-running series where APIs might be reporting seasonal data
+// // For ongoing series, total should be at least as high as aired episodes
+// return airedCount
+// }
+
+// // For normal series, use the maximum from APIs
+// return max(totalFromAPIs, airedCount)
+// }
+
+// // sortSeasonsByAirDate sorts the seasons array chronologically by air date
+// func sortSeasonsByAirDate(seasons *[]types.AnimeSeason) {
+// // First, collect seasons with valid dates
+// seasonsWithDates := make([]types.AnimeSeason, 0)
+// seasonsWithoutDates := make([]types.AnimeSeason, 0)
+
+// for _, season := range *seasons {
+// if season.AiringStatus.From.Year > 0 {
+// seasonsWithDates = append(seasonsWithDates, season)
+// } else {
+// seasonsWithoutDates = append(seasonsWithoutDates, season)
+// }
+// }
+
+// // Sort seasons with dates
+// if len(seasonsWithDates) > 0 {
+// sortedSeasons := make([]types.AnimeSeason, len(seasonsWithDates))
+// copy(sortedSeasons, seasonsWithDates)
+
+// for i := 0; i < len(sortedSeasons)-1; i++ {
+// for j := i + 1; j < len(sortedSeasons); j++ {
+// a := sortedSeasons[i].AiringStatus.From
+// b := sortedSeasons[j].AiringStatus.From
+
+// // Compare years
+// if a.Year > b.Year {
+// sortedSeasons[i], sortedSeasons[j] = sortedSeasons[j], sortedSeasons[i]
+// } else if a.Year == b.Year {
+// // Compare months if years are equal
+// if a.Month > b.Month {
+// sortedSeasons[i], sortedSeasons[j] = sortedSeasons[j], sortedSeasons[i]
+// } else if a.Month == b.Month {
+// // Compare days if months are equal
+// if a.Day > b.Day {
+// sortedSeasons[i], sortedSeasons[j] = sortedSeasons[j], sortedSeasons[i]
+// }
+// }
+// }
+// }
+// }
+
+// // Combine sorted dates with no-date seasons
+// result := append(sortedSeasons, seasonsWithoutDates...)
+// *seasons = result
+// }
+// }
+
+// // generateGenres converts Jikan genre structures to our format
+// func generateGenres(genres, explicitGenres []jikan.JikanGenericMALStructure) []types.AnimeGenres {
+// var animeGenres []types.AnimeGenres
+
+// // Add regular genres
+// for _, genre := range genres {
+// animeGenres = append(animeGenres, types.AnimeGenres{
+// Name: genre.Name,
+// GenreID: genre.MALID,
+// URL: genre.URL,
+// })
+// }
+
+// // Add explicit genres if any
+// for _, genre := range explicitGenres {
+// animeGenres = append(animeGenres, types.AnimeGenres{
+// Name: genre.Name,
+// GenreID: genre.MALID,
+// URL: genre.URL,
+// })
+// }
+
+// return animeGenres
+// }
+
+// // generateStudios converts Jikan studio structures to our format
+// func generateStudios(studios []jikan.JikanGenericMALStructure) []types.AnimeProducer {
+// var animeStudios []types.AnimeProducer
+
+// for _, studio := range studios {
+// animeStudios = append(animeStudios, types.AnimeProducer{
+// Name: studio.Name,
+// MALID: studio.MALID,
+// URL: studio.URL,
+// })
+// }
+
+// return animeStudios
+// }
+
+// // generateProducers converts Jikan producer structures to our format
+// func generateProducers(producers []jikan.JikanGenericMALStructure) []types.AnimeProducer {
+// var animeProducers []types.AnimeProducer
+
+// for _, producer := range producers {
+// animeProducers = append(animeProducers, types.AnimeProducer{
+// Name: producer.Name,
+// MALID: producer.MALID,
+// URL: producer.URL,
+// })
+// }
+
+// return animeProducers
+// }
+
+// // generateLicensors converts Jikan licensor structures to our format
+// func generateLicensors(licensors []jikan.JikanGenericMALStructure) []types.AnimeProducer {
+// var animeLicensors []types.AnimeProducer
+
+// for _, licensor := range licensors {
+// animeLicensors = append(animeLicensors, types.AnimeProducer{
+// Name: licensor.Name,
+// MALID: licensor.MALID,
+// URL: licensor.URL,
+// })
+// }
+
+// return animeLicensors
+// }
+
+// // getAnimeCharacters processes character data from Jikan
+// func getAnimeCharacters(characterResponse *jikan.JikanAnimeCharacterResponse) []types.AnimeCharacter {
+// var characters []types.AnimeCharacter
+
+// for _, entry := range characterResponse.Data {
+// character := types.AnimeCharacter{
+// MALID: entry.Character.MALID,
+// URL: entry.Character.URL,
+// ImageURL: entry.Character.Images.JPG.ImageURL,
+// Name: entry.Character.Name,
+// Role: entry.Role,
+// }
+
+// for _, va := range entry.VoiceActors {
+// character.VoiceActors = append(character.VoiceActors, types.AnimeVoiceActor{
+// MALID: va.Person.MALID,
+// URL: va.Person.URL,
+// Image: va.Person.Images.JPG.ImageURL,
+// Name: va.Person.Name,
+// Language: va.Language,
+// })
+// }
+
+// characters = append(characters, character)
+// }
+
+// return characters
+// }
+
+// // getNextAiringEpisode extracts next airing episode data from AniList
+// func getNextAiringEpisode(anilistAnime *anilist.AnilistAnimeResponse) types.AnimeAiringEpisode {
+// if anilistAnime == nil || anilistAnime.Data.Media.ID == 0 {
+// return types.AnimeAiringEpisode{}
+// }
+
+// // Get the current time to determine the next episode
+// currentTime := time.Now().Unix()
+// nextEpisode := anilistAnime.Data.Media.NextAiringEpisode
+
+// // If AniList provides a valid next airing episode directly, use it
+// if nextEpisode.AiringAt > 0 && nextEpisode.Episode > 0 {
+// return types.AnimeAiringEpisode{
+// AiringAt: nextEpisode.AiringAt,
+// Episode: nextEpisode.Episode,
+// }
+// }
+
+// // If AniList doesn't provide a direct next episode, but we have airing schedule nodes
+// // Find the next episode that hasn't aired yet
+// if len(anilistAnime.Data.Media.AiringSchedule.Nodes) > 0 {
+// var nextAiringEpisode types.AnimeAiringEpisode
+
+// for _, node := range anilistAnime.Data.Media.AiringSchedule.Nodes {
+// if int64(node.AiringAt) > currentTime {
+// // If this is the first future episode we've found, or it airs sooner than our current "next"
+// if nextAiringEpisode.AiringAt == 0 || node.AiringAt < nextAiringEpisode.AiringAt {
+// nextAiringEpisode.AiringAt = node.AiringAt
+// nextAiringEpisode.Episode = node.Episode
+// }
+// }
+// }
+
+// // If we found a next episode
+// if nextAiringEpisode.AiringAt > 0 {
+// return nextAiringEpisode
+// }
+// }
+
+// return types.AnimeAiringEpisode{}
+// }
+
+// // getAnimeSchedule extracts airing schedule data from AniList
+// func getAnimeSchedule(anilistAnime *anilist.AnilistAnimeResponse) []types.AnimeAiringEpisode {
+// if anilistAnime == nil || anilistAnime.Data.Media.AiringSchedule.Nodes == nil {
+// return []types.AnimeAiringEpisode{}
+// }
+
+// var schedule []types.AnimeAiringEpisode
+
+// for _, node := range anilistAnime.Data.Media.AiringSchedule.Nodes {
+// schedule = append(schedule, types.AnimeAiringEpisode{
+// AiringAt: node.AiringAt,
+// Episode: node.Episode,
+// })
+// }
+
+// return schedule
+// }
+
+// // AttachEpisodeDescriptions enhances episode information with external data
+// // Imports the function from the anime utils to use in our service
+// var AttachEpisodeDescriptions = tmdb.AttachEpisodeDescriptions
+
+// // extractLogosFromMALSync extracts logo images from MALSync data
+// func extractLogosFromMALSync(malSyncData *malsync.MALSyncAnimeResponse) types.AnimeLogos {
+// logos := types.AnimeLogos{}
+
+// // Early return if no data
+// if malSyncData == nil {
+// return logos
+// }
+
+// // Check if Crunchyroll data exists in the MALSync response
+// crunchyrollSites, exists := malSyncData.Sites["Crunchyroll"]
+// if !exists || len(crunchyrollSites) == 0 {
+// logger.Log("No Crunchyroll data found in MALSync response", logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+// return logos
+// }
+
+// // Get the Crunchyroll URL from any of the entries
+// crURL := ""
+// for _, site := range crunchyrollSites {
+// crURL = site.URL
+// break // Take the first URL
+// }
+
+// if crURL == "" {
+// logger.Log("No valid Crunchyroll URL found", logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+// return logos
+// }
+
+// // Extract series ID from URL
+// seriesID := extractCrunchyrollSeriesID(crURL)
+// if seriesID == "" {
+// return logos
+// }
+
+// // Define logo sizes
+// logoSizes := map[string]int{
+// "Small": 320,
+// "Medium": 480,
+// "Large": 600,
+// "XLarge": 800,
+// "Original": 1200,
+// }
+
+// // Generate logo URLs
+// logos.Small = fmt.Sprintf("https://imgsrv.crunchyroll.com/cdn-cgi/image/fit=contain,format=auto,quality=85,width=%d/keyart/%s-title_logo-en-us", logoSizes["Small"], seriesID)
+// logos.Medium = fmt.Sprintf("https://imgsrv.crunchyroll.com/cdn-cgi/image/fit=contain,format=auto,quality=85,width=%d/keyart/%s-title_logo-en-us", logoSizes["Medium"], seriesID)
+// logos.Large = fmt.Sprintf("https://imgsrv.crunchyroll.com/cdn-cgi/image/fit=contain,format=auto,quality=85,width=%d/keyart/%s-title_logo-en-us", logoSizes["Large"], seriesID)
+// logos.XLarge = fmt.Sprintf("https://imgsrv.crunchyroll.com/cdn-cgi/image/fit=contain,format=auto,quality=85,width=%d/keyart/%s-title_logo-en-us", logoSizes["XLarge"], seriesID)
+// logos.Original = fmt.Sprintf("https://imgsrv.crunchyroll.com/cdn-cgi/image/fit=contain,format=auto,quality=85,width=%d/keyart/%s-title_logo-en-us", logoSizes["Original"], seriesID)
+
+// logger.Log(fmt.Sprintf("Successfully generated logo URLs for series ID: %s", seriesID), logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+
+// return logos
+// }
+
+// // extractCrunchyrollSeriesID extracts the series ID from a Crunchyroll URL
+// func extractCrunchyrollSeriesID(crURL string) string {
+// logger.Log(fmt.Sprintf("Attempting to extract series ID from URL: %s", crURL), logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+
+// // Direct series URL format
+// if strings.Contains(crURL, "/series/") {
+// parts := strings.Split(crURL, "/series/")
+// if len(parts) < 2 {
+// logger.Log("URL contains /series/ but couldn't extract ID part", logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+// return ""
+// }
+
+// idParts := strings.Split(parts[1], "/")
+// if len(idParts) < 1 {
+// logger.Log("Couldn't extract ID from path segments", logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+// return ""
+// }
+
+// logger.Log(fmt.Sprintf("Found series ID directly in URL: %s", idParts[0]), logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+// return idParts[0]
+// }
+
+// // Need to follow redirect to get series ID
+// logger.Log("URL doesn't contain /series/, following redirect...", logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+
+// // Create a transport that uses modern TLS settings
+// transport := &http.Transport{
+// TLSClientConfig: &tls.Config{
+// MinVersion: tls.VersionTLS12,
+// },
+// ForceAttemptHTTP2: true,
+// }
+
+// client := &http.Client{
+// CheckRedirect: func(req *http.Request, via []*http.Request) error {
+// // Don't follow redirects, just capture the Location header
+// return http.ErrUseLastResponse
+// },
+// Timeout: 10 * time.Second,
+// Transport: transport,
+// }
+
+// // Update HTTP to HTTPS for Crunchyroll URLs if needed
+// if strings.HasPrefix(crURL, "http://www.crunchyroll.com") {
+// crURL = strings.Replace(crURL, "http://", "https://", 1)
+// logger.Log(fmt.Sprintf("Updated URL to HTTPS: %s", crURL), logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+// }
+
+// // Add User-Agent header to mimic a browser
+// req, err := http.NewRequest("GET", crURL, nil)
+// if err != nil {
+// logger.Log(fmt.Sprintf("Failed to create request for Crunchyroll URL: %v", err), logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+// return ""
+// }
+
+// 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")
+// req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml")
+
+// resp, err := client.Do(req)
+// if err != nil {
+// logger.Log(fmt.Sprintf("Failed to get Crunchyroll redirect: %v", err), logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+// return ""
+// }
+// defer resp.Body.Close()
+
+// // Log the status code and response headers for debugging
+// logger.Log(fmt.Sprintf("Crunchyroll response status: %d %s", resp.StatusCode, resp.Status), logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+
+// for name, values := range resp.Header {
+// logger.Log(fmt.Sprintf("Header %s: %s", name, strings.Join(values, ", ")), logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+// }
+
+// // Check for specific status codes for redirects
+// if resp.StatusCode != http.StatusMovedPermanently &&
+// resp.StatusCode != http.StatusFound &&
+// resp.StatusCode != http.StatusTemporaryRedirect &&
+// resp.StatusCode != http.StatusPermanentRedirect {
+// logger.Log(fmt.Sprintf("Unexpected status code from Crunchyroll: %d", resp.StatusCode), logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+
+// // If we got a 200 OK, maybe Crunchyroll served the page directly
+// // Try to extract the series ID from the URL itself as a fallback
+// if resp.StatusCode == http.StatusOK && strings.Contains(crURL, "crunchyroll.com") {
+// // For URLs like http://www.crunchyroll.com/fullmetal-alchemist-brotherhood
+// // Extract the last part as a potential identifier
+// urlParts := strings.Split(crURL, "/")
+// if len(urlParts) > 0 {
+// potentialId := urlParts[len(urlParts)-1]
+// logger.Log(fmt.Sprintf("Extracted potential series ID from original URL: %s", potentialId), logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+// return potentialId
+// }
+// }
+// return ""
+// }
+
+// redirectURL := resp.Header.Get("Location")
+// if redirectURL == "" {
+// logger.Log("No redirect URL found in Crunchyroll response", logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+// return ""
+// }
+
+// logger.Log(fmt.Sprintf("Found redirect URL: %s", redirectURL), logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+
+// // Extract series ID from redirect URL
+// if strings.Contains(redirectURL, "/series/") {
+// parts := strings.Split(redirectURL, "/series/")
+// if len(parts) < 2 {
+// return ""
+// }
+
+// idParts := strings.Split(parts[1], "/")
+// if len(idParts) < 1 {
+// return ""
+// }
+
+// logger.Log(fmt.Sprintf("Successfully extracted series ID from redirect: %s", idParts[0]), logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+// return idParts[0]
+// }
+
+// // For multi-level redirects, try to follow one more time
+// if strings.Contains(redirectURL, "crunchyroll.com") {
+// logger.Log("Trying to follow one more redirect level...", logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+// return extractCrunchyrollSeriesID(redirectURL)
+// }
+
+// // As a fallback for older Crunchyroll URLs like fullmetal-alchemist-brotherhood
+// // Use the last path segment as the ID
+// urlParts := strings.Split(crURL, "/")
+// if len(urlParts) > 0 {
+// potentialId := urlParts[len(urlParts)-1]
+// logger.Log(fmt.Sprintf("Using fallback: extracted ID from original URL: %s", potentialId), logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+// return potentialId
+// }
+
+// logger.Log("Could not extract series ID from Crunchyroll redirect URL", logger.LogOptions{
+// Level: logger.Debug,
+// Prefix: "AnimeAPI",
+// })
+// return ""
+// }
+
+// // FindSeasonMappings finds all anime mappings that belong to the same series based on TVDB ID
+// var FindSeasonMappings = tvdb.FindSeasonMappings