diff options
Diffstat (limited to 'services/anime/helpers.go')
| -rw-r--r-- | services/anime/helpers.go | 1108 |
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 |
