diff options
| author | Bobby <[email protected]> | 2026-02-09 16:29:13 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2026-02-09 16:29:13 +0530 |
| commit | cd9184421327da59c00d0766452e6082055947f2 (patch) | |
| tree | 98e618eb029e947d276d367e9b6d96aaa1865a57 | |
| parent | f382cfae8f0fd2facc7115d1f82dd27ceb1a8258 (diff) | |
| download | metachan-cd9184421327da59c00d0766452e6082055947f2.tar.xz metachan-cd9184421327da59c00d0766452e6082055947f2.zip | |
Refactor AnimeUpdate worker logging and enhance API request for skip times
- Updated worker logging in AnimeUpdate to display worker ID starting from 1 instead of 0.
- Modified the API request in GetSkipTimesForEpisode to include episodeLength=0 in the query parameters for better handling of skip times.
- Added a new BaseModel struct in entities package to standardize model definitions with hidden ID and timestamp fields for JSON responses.
| -rw-r--r-- | controllers/anime.go | 10 | ||||
| -rw-r--r-- | database/migrate.go | 1 | ||||
| -rw-r--r-- | entities/anime.go | 24 | ||||
| -rw-r--r-- | entities/base.go | 15 | ||||
| -rw-r--r-- | entities/episode.go | 30 | ||||
| -rw-r--r-- | entities/genre.go | 4 | ||||
| -rw-r--r-- | entities/mapping.go | 4 | ||||
| -rw-r--r-- | entities/meta.go | 26 | ||||
| -rw-r--r-- | entities/persona.go | 10 | ||||
| -rw-r--r-- | entities/producer.go | 4 | ||||
| -rw-r--r-- | entities/seasons.go | 14 | ||||
| -rw-r--r-- | entities/tasks.go | 6 | ||||
| -rw-r--r-- | services/anime/helpers.go | 556 | ||||
| -rw-r--r-- | services/anime/service.go | 1078 | ||||
| -rw-r--r-- | tasks/aniupdate.task.go | 2 | ||||
| -rw-r--r-- | utils/api/aniskip/aniskip.go | 2 |
16 files changed, 78 insertions, 1708 deletions
diff --git a/controllers/anime.go b/controllers/anime.go index 728db7f..6ce3f1f 100644 --- a/controllers/anime.go +++ b/controllers/anime.go @@ -4,6 +4,7 @@ import ( "errors" "metachan/enums" "metachan/repositories" + "metachan/services" "metachan/utils/meta" "github.com/gofiber/fiber/v2" @@ -19,9 +20,14 @@ func GetAnime(c *fiber.Ctx) error { return BadRequest(c, errors.New("invalid provider")) } - anime, err := repositories.GetAnime(enums.MappingType(provider), id) + mapping, err := repositories.GetAnimeMapping(enums.MappingType(provider), id) if err != nil { - return NotFound(c, errors.New("anime not found")) + return NotFound(c, err) + } + + anime, err := services.GetAnime(&mapping) + if err != nil { + return InternalServerError(c, err) } return c.JSON(anime) diff --git a/database/migrate.go b/database/migrate.go index 9dad67f..44a462c 100644 --- a/database/migrate.go +++ b/database/migrate.go @@ -37,6 +37,7 @@ func migrate() { // Episode entities &entities.Episode{}, + &entities.EpisodeSkipTime{}, &entities.StreamingSource{}, &entities.EpisodeSchedule{}, &entities.NextEpisode{}, diff --git a/entities/anime.go b/entities/anime.go index 9ad2523..e778529 100644 --- a/entities/anime.go +++ b/entities/anime.go @@ -2,22 +2,20 @@ package entities import ( "time" - - "gorm.io/gorm" ) type Anime struct { - gorm.Model + BaseModel MALID int `gorm:"uniqueIndex" json:"id"` - TitleID uint `json:"title_id,omitempty"` - MappingID uint `json:"mapping_id,omitempty"` - ImagesID *uint `json:"images_id,omitempty"` - CoversID *uint `json:"covers_id,omitempty"` - LogosID *uint `json:"logos_id,omitempty"` - ScoresID *uint `json:"scores_id,omitempty"` - AiringStatusID *uint `json:"airing_status_id,omitempty"` - BroadcastID *uint `json:"broadcast_id,omitempty"` - NextAiringID *uint `json:"next_airing_id,omitempty"` + TitleID uint `json:"-"` + MappingID uint `json:"-"` + ImagesID *uint `json:"-"` + CoversID *uint `json:"-"` + LogosID *uint `json:"-"` + ScoresID *uint `json:"-"` + AiringStatusID *uint `json:"-"` + BroadcastID *uint `json:"-"` + NextAiringID *uint `json:"-"` Synopsis string `gorm:"type:text" json:"synopsis,omitempty"` Type string `json:"type,omitempty"` Source string `json:"source,omitempty"` @@ -42,11 +40,11 @@ type Anime struct { Broadcast *Broadcast `gorm:"foreignKey:BroadcastID" json:"broadcast,omitempty"` NextAiring *NextEpisode `gorm:"foreignKey:NextAiringID" json:"next_airing_episode,omitempty"` Genres []Genre `gorm:"many2many:anime_genres;" json:"genres,omitempty"` + Seasons []Season `gorm:"foreignKey:ParentAnimeID" json:"seasons,omitempty"` Producers []Producer `gorm:"many2many:anime_producers;" json:"producers,omitempty"` Studios []Producer `gorm:"many2many:anime_studios;" json:"studios,omitempty"` Licensors []Producer `gorm:"many2many:anime_licensors;" json:"licensors,omitempty"` Episodes []Episode `gorm:"foreignKey:AnimeID" json:"episodes,omitempty"` Characters []Character `gorm:"foreignKey:AnimeID" json:"characters,omitempty"` Schedule []EpisodeSchedule `gorm:"foreignKey:AnimeID;constraint:OnDelete:CASCADE" json:"airing_schedule,omitempty"` - Seasons []Season `gorm:"foreignKey:ParentAnimeID" json:"seasons,omitempty"` } diff --git a/entities/base.go b/entities/base.go new file mode 100644 index 0000000..37d6c30 --- /dev/null +++ b/entities/base.go @@ -0,0 +1,15 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +// BaseModel extends gorm.Model but hides ID and timestamp fields from JSON +type BaseModel struct { + ID uint `gorm:"primarykey" json:"-"` + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` +} diff --git a/entities/episode.go b/entities/episode.go index 9eb13d2..fcf061e 100644 --- a/entities/episode.go +++ b/entities/episode.go @@ -2,15 +2,13 @@ package entities import ( "time" - - "gorm.io/gorm" ) type Episode struct { - gorm.Model + BaseModel EpisodeID string `gorm:"uniqueIndex;size:32" json:"id"` - AnimeID uint `json:"anime_id,omitempty"` - TitleID uint `json:"title_id,omitempty"` + AnimeID uint `json:"-"` + TitleID uint `json:"-"` Description string `gorm:"type:text" json:"description,omitempty"` Aired string `json:"aired,omitempty"` Score float64 `json:"score,omitempty"` @@ -27,40 +25,40 @@ type Episode struct { } type EpisodeSkipTime struct { - gorm.Model - EpisodeID string `gorm:"index;size:32" json:"episode_id"` + BaseModel + EpisodeID string `gorm:"index;size:32" json:"-"` SkipType string `gorm:"index" json:"skip_type"` StartTime float64 `json:"start_time"` EndTime float64 `json:"end_time"` } type EpisodeSchedule struct { - gorm.Model - AnimeID uint `json:"anime_id,omitempty"` + BaseModel + AnimeID uint `json:"-"` AiringAt int `json:"airing_at,omitempty"` Episode int `json:"episode,omitempty"` IsNext bool `gorm:"index" json:"is_next,omitempty"` } type NextEpisode struct { - gorm.Model - AnimeID uint `json:"anime_id,omitempty"` + BaseModel + AnimeID uint `json:"-"` AiringAt int `json:"airing_at,omitempty"` Episode int `json:"episode,omitempty"` } type StreamInfo struct { - gorm.Model - EpisodeID string `gorm:"uniqueIndex:idx_episode_streaming;size:32" json:"episode_id"` - AnimeID uint `gorm:"uniqueIndex:idx_episode_streaming" json:"anime_id,omitempty"` + BaseModel + EpisodeID string `gorm:"uniqueIndex:idx_episode_streaming;size:32" json:"-"` + AnimeID uint `gorm:"uniqueIndex:idx_episode_streaming" json:"-"` SubSources []StreamingSource `gorm:"foreignKey:StreamInfoID;constraint:OnDelete:CASCADE" json:"sub_sources,omitempty"` DubSources []StreamingSource `gorm:"foreignKey:StreamInfoID;constraint:OnDelete:CASCADE" json:"dub_sources,omitempty"` LastFetch time.Time `json:"last_fetch,omitempty"` } type StreamingSource struct { - gorm.Model - StreamInfoID uint `json:"stream_info_id,omitempty"` + BaseModel + StreamInfoID uint `json:"-"` URL string `json:"url,omitempty"` Server string `json:"server,omitempty"` Type string `json:"type,omitempty"` diff --git a/entities/genre.go b/entities/genre.go index ed906ca..f6a5a57 100644 --- a/entities/genre.go +++ b/entities/genre.go @@ -1,9 +1,7 @@ package entities -import "gorm.io/gorm" - type Genre struct { - gorm.Model + BaseModel Name string `json:"name,omitempty"` GenreID int `gorm:"uniqueIndex" json:"genre_id,omitempty"` URL string `json:"url,omitempty"` diff --git a/entities/mapping.go b/entities/mapping.go index eabf3d4..433ce9e 100644 --- a/entities/mapping.go +++ b/entities/mapping.go @@ -2,12 +2,10 @@ package entities import ( "metachan/enums" - - "gorm.io/gorm" ) type Mapping struct { - gorm.Model + BaseModel AniDB int `json:"anidb,omitempty"` Anilist int `json:"anilist,omitempty"` AnimeCountdown int `json:"anime_countdown,omitempty"` diff --git a/entities/meta.go b/entities/meta.go index 57ebae7..2395ddd 100644 --- a/entities/meta.go +++ b/entities/meta.go @@ -1,9 +1,7 @@ package entities -import "gorm.io/gorm" - type Title struct { - gorm.Model + BaseModel English string `json:"english,omitempty"` Japanese string `json:"japanese,omitempty"` Romaji string `json:"romaji,omitempty"` @@ -11,7 +9,7 @@ type Title struct { } type Scores struct { - gorm.Model + BaseModel Score float64 `json:"score,omitempty"` ScoredBy int `json:"scored_by,omitempty"` Rank int `json:"rank,omitempty"` @@ -21,7 +19,7 @@ type Scores struct { } type Date struct { - gorm.Model + BaseModel Day int `json:"day,omitempty"` Month int `json:"month,omitempty"` Year int `json:"year,omitempty"` @@ -29,16 +27,16 @@ type Date struct { } type AiringStatus struct { - gorm.Model - FromID *uint `json:"from_id,omitempty"` - ToID *uint `json:"to_id,omitempty"` + BaseModel + FromID *uint `json:"-"` + ToID *uint `json:"-"` String string `json:"string,omitempty"` From *Date `gorm:"foreignKey:FromID" json:"from,omitempty"` To *Date `gorm:"foreignKey:ToID" json:"to,omitempty"` } type Broadcast struct { - gorm.Model + BaseModel Day string `json:"day,omitempty"` Time string `json:"time,omitempty"` Timezone string `json:"timezone,omitempty"` @@ -46,14 +44,14 @@ type Broadcast struct { } type Images struct { - gorm.Model + BaseModel Small string `json:"small,omitempty"` Large string `json:"large,omitempty"` Original string `json:"original,omitempty"` } type Logos struct { - gorm.Model + BaseModel Small string `json:"small,omitempty"` Medium string `json:"medium,omitempty"` Large string `json:"large,omitempty"` @@ -62,18 +60,18 @@ type Logos struct { } type ExternalURL struct { - gorm.Model + BaseModel Name string `json:"name,omitempty"` URL string `json:"url,omitempty"` } type SimpleTitle struct { - gorm.Model + BaseModel Type string `gorm:"uniqueIndex:idx_simple_title" json:"type,omitempty"` Title string `gorm:"uniqueIndex:idx_simple_title" json:"title,omitempty"` } type SimpleImage struct { - gorm.Model + BaseModel ImageURL string `gorm:"uniqueIndex" json:"image_url,omitempty"` } diff --git a/entities/persona.go b/entities/persona.go index 9928589..481f7af 100644 --- a/entities/persona.go +++ b/entities/persona.go @@ -1,10 +1,8 @@ package entities -import "gorm.io/gorm" - type Character struct { - gorm.Model - AnimeID uint `json:"anime_id,omitempty"` + BaseModel + AnimeID uint `json:"-"` MALID int `json:"mal_id,omitempty"` URL string `json:"url,omitempty"` ImageURL string `json:"image_url,omitempty"` @@ -14,8 +12,8 @@ type Character struct { } type VoiceActor struct { - gorm.Model - CharacterID uint `json:"character_id,omitempty"` + BaseModel + CharacterID uint `json:"-"` MALID int `json:"mal_id,omitempty"` URL string `json:"url,omitempty"` Image string `json:"image_url,omitempty"` diff --git a/entities/producer.go b/entities/producer.go index 356eb06..637697a 100644 --- a/entities/producer.go +++ b/entities/producer.go @@ -1,9 +1,7 @@ package entities -import "gorm.io/gorm" - type Producer struct { - gorm.Model + BaseModel MALID int `gorm:"uniqueIndex" json:"mal_id,omitempty"` URL string `json:"url,omitempty"` Favorites int `json:"favorites,omitempty"` diff --git a/entities/seasons.go b/entities/seasons.go index 1e5f776..f3884e6 100644 --- a/entities/seasons.go +++ b/entities/seasons.go @@ -1,15 +1,13 @@ package entities -import "gorm.io/gorm" - type Season struct { - gorm.Model - ParentAnimeID uint `json:"parent_anime_id,omitempty"` + BaseModel + ParentAnimeID uint `json:"-"` MALID int `json:"mal_id,omitempty"` - TitleID uint `json:"title_id,omitempty"` - ImagesID *uint `json:"images_id,omitempty"` - ScoresID *uint `json:"scores_id,omitempty"` - AiringStatusID *uint `json:"airing_status_id,omitempty"` + TitleID uint `json:"-"` + ImagesID *uint `json:"-"` + ScoresID *uint `json:"-"` + AiringStatusID *uint `json:"-"` Synopsis string `gorm:"type:text" json:"synopsis,omitempty"` Type string `json:"type,omitempty"` Source string `json:"source,omitempty"` diff --git a/entities/tasks.go b/entities/tasks.go index 25eddea..443052b 100644 --- a/entities/tasks.go +++ b/entities/tasks.go @@ -2,12 +2,10 @@ package entities import ( "time" - - "gorm.io/gorm" ) type TaskLog struct { - gorm.Model + BaseModel TaskName string `gorm:"index" json:"task_name,omitempty"` Status string `json:"status,omitempty"` Message string `json:"message,omitempty"` @@ -15,7 +13,7 @@ type TaskLog struct { } type TaskStatus struct { - gorm.Model + BaseModel TaskName string `gorm:"uniqueIndex;not null" json:"task_name"` IsCompleted bool `gorm:"default:false" json:"is_completed,omitempty"` LastRunAt time.Time `json:"last_run_at,omitempty"` diff --git a/services/anime/helpers.go b/services/anime/helpers.go deleted file mode 100644 index 061ffae..0000000 --- a/services/anime/helpers.go +++ /dev/null @@ -1,556 +0,0 @@ -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 diff --git a/services/anime/service.go b/services/anime/service.go deleted file mode 100644 index 830836c..0000000 --- a/services/anime/service.go +++ /dev/null @@ -1,1078 +0,0 @@ -package anime - -// import ( -// "fmt" -// "metachan/database" -// "metachan/entities" -// "metachan/types" -// "metachan/utils/api/anilist" -// "metachan/utils/api/jikan" -// "metachan/utils/api/malsync" -// "metachan/utils/api/streaming" -// "metachan/utils/api/tmdb" -// "metachan/utils/api/tvdb" -// "metachan/utils/concurrency" -// "metachan/utils/logger" -// "strings" -// "time" -// ) - -// // Service provides high-level operations for anime data -// type Service struct { -// jikanClient *jikan.JikanClient -// streamingClient *streaming.AllAnimeClient -// anilistClient *anilist.AniListClient -// malsyncClient *malsync.MALSyncClient -// } - -// // NewService creates a new anime service -// func NewService() *Service { -// return &Service{ -// jikanClient: jikan.NewJikanClient(), -// streamingClient: streaming.NewAllAnimeClient(), -// anilistClient: anilist.NewAniListClient(), -// malsyncClient: malsync.NewMALSyncClient(), -// } -// } - -// // GetAnimeDetailsWithSource fetches comprehensive anime details with source information -// func (s *Service) GetAnimeDetailsWithSource(mapping *entities.AnimeMapping, source string) (*types.Anime, error) { -// if mapping == nil { -// return nil, fmt.Errorf("anime mapping is nil") -// } - -// startTime := time.Now() -// defer func() { -// duration := time.Since(startTime) -// logger.Log(fmt.Sprintf("GetAnimeDetails (%s) execution time: %s", source, duration), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) -// }() - -// malID := mapping.MAL - -// // For updater source, always fetch fresh data and skip cache -// if source != "updater" { -// // First, check if we have an existing version in the database -// anime, err := database.GetAnimeByMALID(malID) -// if err == nil { -// logger.Log(fmt.Sprintf("Found existing anime data (MAL ID: %d), returning stored data", malID), logger.LogOptions{ -// Level: logger.Info, -// Prefix: "AnimeDB", -// }) - -// // Ensure mappings are attached properly -// anime.Mappings = types.AnimeMappings{ -// AniDB: mapping.AniDB, -// Anilist: mapping.Anilist, -// AnimeCountdown: mapping.AnimeCountdown, -// AnimePlanet: mapping.AnimePlanet, -// AniSearch: mapping.AniSearch, -// IMDB: mapping.IMDB, -// Kitsu: mapping.Kitsu, -// LiveChart: mapping.LiveChart, -// NotifyMoe: mapping.NotifyMoe, -// Simkl: mapping.Simkl, -// TMDB: mapping.TMDB, -// TVDB: mapping.TVDB, -// } - -// return anime, nil -// } -// } else { -// logger.Log(fmt.Sprintf("Bypassing database check for anime (MAL ID: %d) - source: %s", malID, source), logger.LogOptions{ -// Level: logger.Info, -// Prefix: "AnimeAPI", -// }) -// } - -// // Rest of the implementation is the same as GetAnimeDetails -// logger.Log(fmt.Sprintf("No existing data for anime (MAL ID: %d), fetching fresh data", malID), logger.LogOptions{ -// Level: logger.Info, -// Prefix: "AnimeAPI", -// }) - -// // Create the different types of functions for proper Go generic type inference -// animeFunc := func() (*jikan.JikanAnimeResponse, error) { -// return s.jikanClient.GetFullAnime(malID) -// } - -// episodesFunc := func() (*jikan.JikanAnimeEpisodeResponse, error) { -// return s.jikanClient.GetAnimeEpisodes(malID) -// } - -// charactersFunc := func() (*jikan.JikanAnimeCharacterResponse, error) { -// return s.jikanClient.GetAnimeCharacters(malID) -// } - -// fetchStartTime := time.Now() -// // Use separate results variables for each type -// animeResult := concurrency.Parallel(animeFunc)[0] -// episodesResult := concurrency.Parallel(episodesFunc)[0] -// charactersResult := concurrency.Parallel(charactersFunc)[0] -// logger.Log(fmt.Sprintf("Initial parallel API fetch time: %s", time.Since(fetchStartTime)), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) - -// // Extract results and handle errors -// anime := animeResult.Value -// if animeResult.Error != nil { -// return nil, fmt.Errorf("failed to get anime details: %w", animeResult.Error) -// } - -// episodes := episodesResult.Value -// if episodesResult.Error != nil { -// return nil, fmt.Errorf("failed to get anime episodes: %w", episodesResult.Error) -// } - -// characterResponse := charactersResult.Value -// if charactersResult.Error != nil { -// return nil, fmt.Errorf("failed to get anime characters: %w", charactersResult.Error) -// } - -// // Get Anilist and MALSync data in parallel if available -// var anilistAnime *anilist.AnilistAnimeResponse -// var malSyncData *malsync.MALSyncAnimeResponse - -// anilistStartTime := time.Now() -// if mapping.Anilist != 0 { -// // We need separate functions for each type for proper type inference -// anilistFunc := func() (*anilist.AnilistAnimeResponse, error) { -// return s.anilistClient.GetAnime(mapping.Anilist) -// } - -// malsyncFunc := func() (*malsync.MALSyncAnimeResponse, error) { -// return s.malsyncClient.GetAnimeByMALID(malID) -// } - -// // Execute them separately to avoid type errors -// anilistResult := concurrency.Parallel(anilistFunc)[0] -// malsyncResult := concurrency.Parallel(malsyncFunc)[0] - -// // Extract AniList result -// if anilistResult.Error == nil { -// anilistAnime = anilistResult.Value -// logger.Log(fmt.Sprintf("Successfully fetched AniList data for ID %d", mapping.Anilist), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) -// } else { -// logger.Log(fmt.Sprintf("Failed to fetch AniList data: %v", anilistResult.Error), logger.LogOptions{ -// Level: logger.Warn, -// Prefix: "AnimeAPI", -// }) -// } - -// // Extract MALSync result -// if malsyncResult.Error == nil { -// malSyncData = malsyncResult.Value -// } else { -// logger.Log(fmt.Sprintf("Failed to fetch MALSync data: %v", malsyncResult.Error), logger.LogOptions{ -// Level: logger.Warn, -// Prefix: "AnimeAPI", -// }) -// } -// } else { -// logger.Log(fmt.Sprintf("No AniList ID available for MAL ID %d", malID), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) -// // If no AniList ID, just fetch MALSync data -// malSyncData, _ = s.malsyncClient.GetAnimeByMALID(malID) -// } -// logger.Log(fmt.Sprintf("AniList and MALSync fetch time: %s", time.Since(anilistStartTime)), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) - -// // Process episode data in parallel with seasons and other operations -// episodeDataChan := make(chan []types.AnimeSingleEpisode, 1) -// subbedCountChan := make(chan int, 1) -// dubbedCountChan := make(chan int, 1) -// tmdbErrorChan := make(chan error, 1) - -// episodeProcessingStartTime := time.Now() -// go func() { -// defer close(episodeDataChan) -// defer close(subbedCountChan) -// defer close(dubbedCountChan) -// defer close(tmdbErrorChan) - -// var enrichedEpisodes []types.AnimeSingleEpisode -// var tmdbErr error - -// // Check anime type - use different sources for movies vs TV shows -// animeType := string(mapping.Type) - -// if (animeType == "MOVIE" || animeType == "Movie") && mapping.TMDB != 0 { -// // For movies with TMDB mapping, use TMDB to get movie details as a single episode -// logger.Log(fmt.Sprintf("Detected movie type with TMDB ID %d, fetching from TMDB for: %s", mapping.TMDB, anime.Data.Title), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) - -// enrichedEpisodes, tmdbErr = tmdb.GetMovieAsEpisode( -// anime.Data.Title, -// anime.Data.TitleEnglish, -// mapping.TMDB, -// anime.Data.MALID, -// anime.Data.TitleJapanese, -// anime.Data.Score, -// ) -// if tmdbErr != nil { -// logger.Log(fmt.Sprintf("Failed to get movie from TMDB: %v, falling back to basic episode", tmdbErr), logger.LogOptions{ -// Level: logger.Warn, -// Prefix: "AnimeAPI", -// }) -// // Fallback to basic episode generation -// basicEpisodes := generateBasicEpisodes(anime.Data.MALID, episodes.Data) -// enrichedEpisodes = basicEpisodes -// } -// } else { -// // For TV shows, prefer TVDB over TMDB -// var usedfallback bool - -// if mapping.TVDB != 0 { -// // Try TVDB first for TV shows -// logger.Log(fmt.Sprintf("Using TVDB for TV show episodes (TVDB ID: %d)", mapping.TVDB), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) - -// tvdbEpisodes, tvdbErr := tvdb.GetSeriesEpisodes(mapping.TVDB) -// if tvdbErr == nil && len(tvdbEpisodes) > 0 { -// enrichedEpisodes = tvdb.ConvertTVDBEpisodesToAnimeEpisodes(tvdbEpisodes) -// logger.Log(fmt.Sprintf("Successfully fetched %d episodes from TVDB", len(enrichedEpisodes)), logger.LogOptions{ -// Level: logger.Success, -// Prefix: "TVDB", -// }) -// } else { -// logger.Log(fmt.Sprintf("TVDB fetch failed or returned no episodes: %v, falling back to TMDB", tvdbErr), logger.LogOptions{ -// Level: logger.Warn, -// Prefix: "TVDB", -// }) -// usedfallback = true -// } -// } else { -// logger.Log("No TVDB ID available, using TMDB for episodes", logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) -// usedfallback = true -// } - -// // Fallback to TMDB if TVDB failed or wasn't available -// if usedfallback { -// basicEpisodes := generateBasicEpisodes(anime.Data.MALID, episodes.Data) -// logger.Log(fmt.Sprintf("Generated basic episodes: %d", len(basicEpisodes)), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) - -// logger.Log(fmt.Sprintf("Starting TMDB enrichment for %d episodes", len(basicEpisodes)), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) -// enrichStart := time.Now() - -// enrichedEpisodes, tmdbErr = AttachEpisodeDescriptions(anime.Data.Title, basicEpisodes, anime.Data.TitleEnglish, mapping.TMDB) - -// logger.Log(fmt.Sprintf("TMDB enrichment execution time: %s", time.Since(enrichStart)), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) -// } -// } - -// tmdbErrorChan <- tmdbErr - -// // Get subbed and dubbed episode counts in bulk with a single API call (much faster) -// subCount, dubCount := 0, 0 -// searchTitle := anime.Data.Title - -// startStreamingCheck := time.Now() -// logger.Log("Fetching streaming episode counts...", logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) - -// var err error - -// // Try primary title first -// subCount, dubCount, err = s.streamingClient.GetStreamingCounts(searchTitle) - -// // If primary title fails, try with English title -// if err != nil && anime.Data.TitleEnglish != "" { -// englishTitle := strings.TrimPrefix(anime.Data.TitleEnglish, "English: ") -// logger.Log(fmt.Sprintf("Retrying with English title: %s", englishTitle), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) -// subCount, dubCount, err = s.streamingClient.GetStreamingCounts(englishTitle) -// } - -// // If English title fails, try with Romaji title from Anilist -// if err != nil && anilistAnime != nil && anilistAnime.Data.Media.Title.Romaji != "" { -// romajiTitle := anilistAnime.Data.Media.Title.Romaji -// logger.Log(fmt.Sprintf("Retrying with Romaji title: %s", romajiTitle), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) -// subCount, dubCount, err = s.streamingClient.GetStreamingCounts(romajiTitle) -// } - -// // If Romaji fails, try synonyms -// if err != nil && len(anime.Data.TitleSynonyms) > 0 { -// for _, synonym := range anime.Data.TitleSynonyms { -// if synonym == "" { -// continue -// } -// logger.Log(fmt.Sprintf("Retrying with synonym: %s", synonym), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) -// subCount, dubCount, err = s.streamingClient.GetStreamingCounts(synonym) -// if err == nil { -// break // Found a match -// } -// } -// } - -// // Log the final error if all attempts failed -// if err != nil { -// logger.Log(fmt.Sprintf("Failed to fetch streaming counts after all attempts: %v", err), logger.LogOptions{ -// Level: logger.Warn, -// Prefix: "AnimeAPI", -// }) -// } - -// logger.Log(fmt.Sprintf("Streaming count check took %s. Subbed: %d, Dubbed: %d", -// time.Since(startStreamingCheck), subCount, dubCount), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) - -// episodeDataChan <- enrichedEpisodes -// subbedCountChan <- subCount -// dubbedCountChan <- dubCount -// }() - -// // Get seasons information if TVDB ID is available -// seasonsStartTime := time.Now() -// var seasons []types.AnimeSeason -// if mapping.TVDB != 0 { -// logger.Log(fmt.Sprintf("Finding season mappings for TVDB ID %d", mapping.TVDB), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "TVDB", -// }) -// seasonMappings, err := tvdb.FindSeasonMappings(mapping.TVDB) -// if err == nil && len(seasonMappings) > 0 { -// logger.Log(fmt.Sprintf("Found %d season mappings for TVDB ID %d", len(seasonMappings), mapping.TVDB), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "TVDB", -// }) -// seasons = s.getSeasonDetails(&seasonMappings, malID) -// } -// } -// logger.Log(fmt.Sprintf("Seasons fetch time: %s", time.Since(seasonsStartTime)), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) - -// // Get logos data from MALSync data -// logos := extractLogosFromMALSync(malSyncData) - -// // Extract character data -// characters := getAnimeCharacters(characterResponse) - -// // Extract episode count, next airing episode, and schedule -// var nextAiringEpisode types.AnimeAiringEpisode -// var schedule []types.AnimeAiringEpisode - -// if anilistAnime != nil { -// nextAiringEpisode = getNextAiringEpisode(anilistAnime) -// schedule = getAnimeSchedule(anilistAnime) -// } - -// // Wait for episode data to complete -// logger.Log(fmt.Sprintf("Waiting for episode data processing (started %s ago)", time.Since(episodeProcessingStartTime)), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) -// episodeWaitStartTime := time.Now() -// episodeData := <-episodeDataChan -// subbedCount := <-subbedCountChan -// dubbedCount := <-dubbedCountChan -// tmdbError := <-tmdbErrorChan -// logger.Log(fmt.Sprintf("Episode data wait time: %s (total episode processing time: %s)", -// time.Since(episodeWaitStartTime), -// time.Since(episodeProcessingStartTime)), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) - -// // Assemble final anime details -// animeDetails := &types.Anime{ -// MALID: malID, -// Titles: types.AnimeTitles{ -// Romaji: anime.Data.Title, -// English: anime.Data.TitleEnglish, -// Japanese: anime.Data.TitleJapanese, -// Synonyms: anime.Data.TitleSynonyms, -// }, -// Synopsis: anime.Data.Synopsis, -// Type: types.AniSyncType(mapping.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, -// 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{}, -// 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, -// Episodes: types.AnimeEpisodes{ -// Total: getEpisodeCountWithAiredFallback(anime, anilistAnime, len(episodes.Data)), -// Aired: len(episodes.Data), -// Subbed: subbedCount, -// Dubbed: dubbedCount, -// Episodes: episodeData, -// }, -// NextAiringEpisode: nextAiringEpisode, -// AiringSchedule: schedule, -// Characters: characters, -// Mappings: types.AnimeMappings{ -// AniDB: mapping.AniDB, -// Anilist: mapping.Anilist, -// AnimeCountdown: mapping.AnimeCountdown, -// AnimePlanet: mapping.AnimePlanet, -// AniSearch: mapping.AniSearch, -// IMDB: mapping.IMDB, -// Kitsu: mapping.Kitsu, -// LiveChart: mapping.LiveChart, -// NotifyMoe: mapping.NotifyMoe, -// Simkl: mapping.Simkl, -// TMDB: mapping.TMDB, -// TVDB: mapping.TVDB, -// }, -// } - -// // Add AniList cover images and color if available -// if anilistAnime != nil && anilistAnime.Data.Media.ID > 0 { -// logger.Log("Setting covers and color from AniList data", logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) - -// // Create debug logs for the data -// coverImage := anilistAnime.Data.Media.CoverImage - -// // Explicitly set the cover images, ensuring we don't have empty values -// animeDetails.Covers = types.AnimeImages{ -// Small: coverImage.Medium, -// Large: coverImage.Large, -// Original: coverImage.ExtraLarge, -// } - -// // For color, also make sure it's not empty -// if coverImage.Color != "" { -// animeDetails.Color = coverImage.Color -// logger.Log(fmt.Sprintf("Set color to: %s", coverImage.Color), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) -// } -// } else { -// logger.Log("No valid AniList data available for covers and color", logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeAPI", -// }) -// } - -// // Save the anime to database only if TMDB didn't fail -// if tmdbError == nil { -// go func() { -// if err := database.SaveAnimeToDatabase(animeDetails); err != nil { -// logger.Log(fmt.Sprintf("Failed to save anime to database: %v", err), logger.LogOptions{ -// Level: logger.Error, -// Prefix: "AnimeDB", -// }) -// } else { -// logger.Log(fmt.Sprintf("Successfully saved anime (MAL ID: %d) to database", malID), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeDB", -// }) -// } -// }() -// } else { -// logger.Log(fmt.Sprintf("Skipping anime database save due to TMDB error: %v", tmdbError), logger.LogOptions{ -// Level: logger.Warn, -// Prefix: "AnimeDB", -// }) -// } - -// return animeDetails, nil -// } - -// // GetAnimeDetails fetches comprehensive anime details -// func (s *Service) GetAnimeDetails(mapping *entities.AnimeMapping) (*types.Anime, error) { -// return s.GetAnimeDetailsWithSource(mapping, "api") -// } - -// // GetAnimeByGenre fetches anime list by genre with pagination -// func (s *Service) GetAnimeByGenre(genreID int, page int, limit int) ([]types.Anime, struct { -// LastVisiblePage int `json:"last_visible_page"` -// HasNextPage bool `json:"has_next_page"` -// CurrentPage int `json:"current_page"` -// Items struct { -// Count int `json:"count"` -// Total int `json:"total"` -// PerPage int `json:"per_page"` -// } `json:"items"` -// }, error) { -// // Fetch anime list from Jikan -// response, err := s.jikanClient.GetAnimeByGenre(genreID, page, limit) -// if err != nil { -// return nil, struct { -// LastVisiblePage int `json:"last_visible_page"` -// HasNextPage bool `json:"has_next_page"` -// CurrentPage int `json:"current_page"` -// Items struct { -// Count int `json:"count"` -// Total int `json:"total"` -// PerPage int `json:"per_page"` -// } `json:"items"` -// }{}, fmt.Errorf("failed to fetch anime by genre: %w", err) -// } - -// animeList := make([]types.Anime, 0, len(response.Data)) -// stalenessThreshold := 7 * 24 * time.Hour // 7 days - -// // Process each anime - check DB first, fetch only if missing/stale -// for _, item := range response.Data { -// // Try to get from database first -// cachedAnime, err := database.GetAnimeByMALID(item.MALID) -// if err == nil && cachedAnime != nil { -// // Check if data is fresh (updated within last 7 days) -// var dbAnime entities.Anime -// if dbErr := database.DB.Where("mal_id = ?", item.MALID).First(&dbAnime).Error; dbErr == nil { -// if time.Since(dbAnime.LastUpdated) < stalenessThreshold { -// // Data is fresh, use cached version -// cachedAnime.Seasons = nil -// cachedAnime.Episodes.Episodes = nil -// cachedAnime.NextAiringEpisode = types.AnimeAiringEpisode{} -// cachedAnime.AiringSchedule = nil -// cachedAnime.Characters = nil -// animeList = append(animeList, *cachedAnime) -// continue -// } -// } -// } - -// // Data is missing or stale, fetch from API -// mapping, err := database.GetAnimeMappingViaMALID(item.MALID) -// if err != nil { -// mapping = &entities.AnimeMapping{MAL: item.MALID} -// } - -// fullAnime, err := s.GetAnimeDetailsWithSource(mapping, "genre_listing") -// if err != nil { -// logger.Log(fmt.Sprintf("Failed to fetch full anime for MAL ID %d: %v", item.MALID, err), logger.LogOptions{ -// Level: logger.Error, -// Prefix: "AnimeService", -// }) -// // If fetch fails but we have cached data (even if stale), use it -// if cachedAnime != nil { -// cachedAnime.Seasons = nil -// cachedAnime.Episodes.Episodes = nil -// cachedAnime.NextAiringEpisode = types.AnimeAiringEpisode{} -// cachedAnime.AiringSchedule = nil -// cachedAnime.Characters = nil -// animeList = append(animeList, *cachedAnime) -// } -// continue -// } - -// // Clear fields not needed in genre listing (omitempty will handle JSON exclusion) -// fullAnime.Seasons = nil -// fullAnime.Episodes.Episodes = nil -// fullAnime.NextAiringEpisode = types.AnimeAiringEpisode{} -// fullAnime.AiringSchedule = nil -// fullAnime.Characters = nil - -// animeList = append(animeList, *fullAnime) -// } - -// return animeList, response.Pagination, nil -// } - -// // GetAnimeByProducer fetches anime list by producer with pagination -// func (s *Service) GetAnimeByProducer(producerID int, page int, limit int) ([]types.Anime, struct { -// LastVisiblePage int `json:"last_visible_page"` -// HasNextPage bool `json:"has_next_page"` -// CurrentPage int `json:"current_page"` -// Items struct { -// Count int `json:"count"` -// Total int `json:"total"` -// PerPage int `json:"per_page"` -// } `json:"items"` -// }, error) { -// // Fetch anime list from Jikan -// response, err := s.jikanClient.GetAnimeByProducer(producerID, page, limit) -// if err != nil { -// return nil, struct { -// LastVisiblePage int `json:"last_visible_page"` -// HasNextPage bool `json:"has_next_page"` -// CurrentPage int `json:"current_page"` -// Items struct { -// Count int `json:"count"` -// Total int `json:"total"` -// PerPage int `json:"per_page"` -// } `json:"items"` -// }{}, fmt.Errorf("failed to fetch anime by producer: %w", err) -// } - -// animeList := make([]types.Anime, 0, len(response.Data)) -// stalenessThreshold := 7 * 24 * time.Hour // 7 days - -// // Process each anime - check DB first, fetch only if missing/stale -// for _, item := range response.Data { -// // Try to get from database first -// cachedAnime, err := database.GetAnimeByMALID(item.MALID) -// if err == nil && cachedAnime != nil { -// // Check if data is fresh (updated within last 7 days) -// var dbAnime entities.Anime -// if dbErr := database.DB.Where("mal_id = ?", item.MALID).First(&dbAnime).Error; dbErr == nil { -// if time.Since(dbAnime.LastUpdated) < stalenessThreshold { -// // Data is fresh, use cached version -// cachedAnime.Seasons = nil -// cachedAnime.Episodes.Episodes = nil -// cachedAnime.NextAiringEpisode = types.AnimeAiringEpisode{} -// cachedAnime.AiringSchedule = nil -// cachedAnime.Characters = nil -// animeList = append(animeList, *cachedAnime) -// continue -// } -// } -// } - -// // Data is missing or stale, fetch from API -// mapping, err := database.GetAnimeMappingViaMALID(item.MALID) -// if err != nil { -// mapping = &entities.AnimeMapping{MAL: item.MALID} -// } - -// fullAnime, err := s.GetAnimeDetailsWithSource(mapping, "producer_listing") -// if err != nil { -// logger.Log(fmt.Sprintf("Failed to fetch full anime for MAL ID %d: %v", item.MALID, err), logger.LogOptions{ -// Level: logger.Error, -// Prefix: "AnimeService", -// }) -// // If fetch fails but we have cached data (even if stale), use it -// if cachedAnime != nil { -// cachedAnime.Seasons = nil -// cachedAnime.Episodes.Episodes = nil -// cachedAnime.NextAiringEpisode = types.AnimeAiringEpisode{} -// cachedAnime.AiringSchedule = nil -// cachedAnime.Characters = nil -// animeList = append(animeList, *cachedAnime) -// } -// continue -// } - -// // Clear fields not needed in producer listing (omitempty will handle JSON exclusion) -// fullAnime.Seasons = nil -// fullAnime.Episodes.Episodes = nil -// fullAnime.NextAiringEpisode = types.AnimeAiringEpisode{} -// fullAnime.AiringSchedule = nil -// fullAnime.Characters = nil - -// animeList = append(animeList, *fullAnime) -// } - -// return animeList, response.Pagination, nil -// } - -// func (s *Service) GetAnimeByStudio(studioID int, page int, limit int) ([]types.Anime, struct { -// LastVisiblePage int `json:"last_visible_page"` -// HasNextPage bool `json:"has_next_page"` -// CurrentPage int `json:"current_page"` -// Items struct { -// Count int `json:"count"` -// Total int `json:"total"` -// PerPage int `json:"per_page"` -// } `json:"items"` -// }, error) { -// // Fetch anime list from Jikan -// response, err := s.jikanClient.GetAnimeByStudio(studioID, page, limit) -// if err != nil { -// return nil, struct { -// LastVisiblePage int `json:"last_visible_page"` -// HasNextPage bool `json:"has_next_page"` -// CurrentPage int `json:"current_page"` -// Items struct { -// Count int `json:"count"` -// Total int `json:"total"` -// PerPage int `json:"per_page"` -// } `json:"items"` -// }{}, fmt.Errorf("failed to fetch anime by studio: %w", err) -// } - -// animeList := make([]types.Anime, 0, len(response.Data)) -// stalenessThreshold := 7 * 24 * time.Hour // 7 days - -// // Process each anime - check DB first, fetch only if missing/stale -// for _, item := range response.Data { -// // Try to get from database first -// cachedAnime, err := database.GetAnimeByMALID(item.MALID) -// if err == nil && cachedAnime != nil { -// // Check if data is fresh (updated within last 7 days) -// var dbAnime entities.Anime -// if dbErr := database.DB.Where("mal_id = ?", item.MALID).First(&dbAnime).Error; dbErr == nil { -// if time.Since(dbAnime.LastUpdated) < stalenessThreshold { -// // Data is fresh, use cached version -// cachedAnime.Seasons = nil -// cachedAnime.Episodes.Episodes = nil -// cachedAnime.NextAiringEpisode = types.AnimeAiringEpisode{} -// cachedAnime.AiringSchedule = nil -// cachedAnime.Characters = nil -// animeList = append(animeList, *cachedAnime) -// continue -// } -// } -// } - -// // Data is missing or stale, fetch from API -// mapping, err := database.GetAnimeMappingViaMALID(item.MALID) -// if err != nil { -// mapping = &entities.AnimeMapping{MAL: item.MALID} -// } - -// fullAnime, err := s.GetAnimeDetailsWithSource(mapping, "studio_listing") -// if err != nil { -// logger.Log(fmt.Sprintf("Failed to fetch full anime for MAL ID %d: %v", item.MALID, err), logger.LogOptions{ -// Level: logger.Error, -// Prefix: "AnimeService", -// }) -// // If fetch fails but we have cached data (even if stale), use it -// if cachedAnime != nil { -// cachedAnime.Seasons = nil -// cachedAnime.Episodes.Episodes = nil -// cachedAnime.NextAiringEpisode = types.AnimeAiringEpisode{} -// cachedAnime.AiringSchedule = nil -// cachedAnime.Characters = nil -// animeList = append(animeList, *cachedAnime) -// } -// continue -// } - -// // Clear fields not needed in studio listing (omitempty will handle JSON exclusion) -// fullAnime.Seasons = nil -// fullAnime.Episodes.Episodes = nil -// fullAnime.NextAiringEpisode = types.AnimeAiringEpisode{} -// fullAnime.AiringSchedule = nil -// fullAnime.Characters = nil - -// animeList = append(animeList, *fullAnime) -// } - -// return animeList, response.Pagination, nil -// } - -// func (s *Service) GetAnimeByLicensor(licensorID int, page int, limit int) ([]types.Anime, struct { -// LastVisiblePage int `json:"last_visible_page"` -// HasNextPage bool `json:"has_next_page"` -// CurrentPage int `json:"current_page"` -// Items struct { -// Count int `json:"count"` -// Total int `json:"total"` -// PerPage int `json:"per_page"` -// } `json:"items"` -// }, error) { -// // Fetch anime list from Jikan -// response, err := s.jikanClient.GetAnimeByLicensor(licensorID, page, limit) -// if err != nil { -// return nil, struct { -// LastVisiblePage int `json:"last_visible_page"` -// HasNextPage bool `json:"has_next_page"` -// CurrentPage int `json:"current_page"` -// Items struct { -// Count int `json:"count"` -// Total int `json:"total"` -// PerPage int `json:"per_page"` -// } `json:"items"` -// }{}, fmt.Errorf("failed to fetch anime by licensor: %w", err) -// } - -// animeList := make([]types.Anime, 0, len(response.Data)) -// stalenessThreshold := 7 * 24 * time.Hour // 7 days - -// // Process each anime - check DB first, fetch only if missing/stale -// for _, item := range response.Data { -// // Try to get from database first -// cachedAnime, err := database.GetAnimeByMALID(item.MALID) -// if err == nil && cachedAnime != nil { -// // Check if data is fresh (updated within last 7 days) -// var dbAnime entities.Anime -// if dbErr := database.DB.Where("mal_id = ?", item.MALID).First(&dbAnime).Error; dbErr == nil { -// if time.Since(dbAnime.LastUpdated) < stalenessThreshold { -// // Data is fresh, use cached version -// cachedAnime.Seasons = nil -// cachedAnime.Episodes.Episodes = nil -// cachedAnime.NextAiringEpisode = types.AnimeAiringEpisode{} -// cachedAnime.AiringSchedule = nil -// cachedAnime.Characters = nil -// animeList = append(animeList, *cachedAnime) -// continue -// } -// } -// } - -// // Data is missing or stale, fetch from API -// mapping, err := database.GetAnimeMappingViaMALID(item.MALID) -// if err != nil { -// mapping = &entities.AnimeMapping{MAL: item.MALID} -// } - -// fullAnime, err := s.GetAnimeDetailsWithSource(mapping, "licensor_listing") -// if err != nil { -// logger.Log(fmt.Sprintf("Failed to fetch full anime for MAL ID %d: %v", item.MALID, err), logger.LogOptions{ -// Level: logger.Error, -// Prefix: "AnimeService", -// }) -// // If fetch fails but we have cached data (even if stale), use it -// if cachedAnime != nil { -// cachedAnime.Seasons = nil -// cachedAnime.Episodes.Episodes = nil -// cachedAnime.NextAiringEpisode = types.AnimeAiringEpisode{} -// cachedAnime.AiringSchedule = nil -// cachedAnime.Characters = nil -// animeList = append(animeList, *cachedAnime) -// } -// continue -// } - -// // Clear fields not needed in licensor listing (omitempty will handle JSON exclusion) -// fullAnime.Seasons = nil -// fullAnime.Episodes.Episodes = nil -// fullAnime.NextAiringEpisode = types.AnimeAiringEpisode{} -// fullAnime.AiringSchedule = nil -// fullAnime.Characters = nil - -// animeList = append(animeList, *fullAnime) -// } - -// return animeList, response.Pagination, nil -// } - -// // GetEpisodeStreaming fetches streaming sources for a specific episode -// func (s *Service) GetEpisodeStreaming(title string, episodeNumber int, episodeID string, animeID uint) (*types.AnimeStreaming, error) { -// // Try to get from database first -// cached, err := database.GetEpisodeStreaming(episodeID, animeID) -// if err == nil && cached != nil { -// logger.Log(fmt.Sprintf("Using cached streaming data for episode %d", episodeNumber), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeService", -// }) - -// result := &types.AnimeStreaming{ -// Sub: make([]types.AnimeStreamingSource, len(cached.SubSources)), -// Dub: make([]types.AnimeStreamingSource, len(cached.DubSources)), -// } - -// for i, source := range cached.SubSources { -// result.Sub[i] = types.AnimeStreamingSource{ -// URL: source.URL, -// Server: source.Server, -// Type: source.Type, -// } -// } - -// for i, source := range cached.DubSources { -// result.Dub[i] = types.AnimeStreamingSource{ -// URL: source.URL, -// Server: source.Server, -// Type: source.Type, -// } -// } - -// return result, nil -// } - -// // If not in cache or stale, fetch from API -// logger.Log(fmt.Sprintf("Fetching fresh streaming data for episode %d from API", episodeNumber), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeService", -// }) - -// streaming, err := s.streamingClient.GetStreamingSources(title, episodeNumber) -// if err != nil { -// return nil, fmt.Errorf("failed to get streaming sources: %w", err) -// } - -// // Convert streaming API type to types package type -// result := &types.AnimeStreaming{ -// Sub: make([]types.AnimeStreamingSource, len(streaming.Sub)), -// Dub: make([]types.AnimeStreamingSource, len(streaming.Dub)), -// } - -// for i, source := range streaming.Sub { -// result.Sub[i] = types.AnimeStreamingSource{ -// URL: source.URL, -// Server: source.Server, -// Type: source.Type, -// } -// } - -// for i, source := range streaming.Dub { -// result.Dub[i] = types.AnimeStreamingSource{ -// URL: source.URL, -// Server: source.Server, -// Type: source.Type, -// } -// } - -// // Save to database for future requests -// if err := database.SaveEpisodeStreaming(episodeID, animeID, result.Sub, result.Dub); err != nil { -// logger.Log(fmt.Sprintf("Failed to cache streaming data: %v", err), logger.LogOptions{ -// Level: logger.Warn, -// Prefix: "AnimeService", -// }) -// } else { -// logger.Log(fmt.Sprintf("Cached streaming data for episode %d", episodeNumber), logger.LogOptions{ -// Level: logger.Debug, -// Prefix: "AnimeService", -// }) -// } - -// return result, nil -// } - -// // getSeasonDetails fetches details for anime seasons -// func (s *Service) getSeasonDetails(mappings *[]entities.AnimeMapping, currentMALID int) []types.AnimeSeason { -// // Helper function to fetch anime details for a single mapping -// fetchSeason := func(mapping entities.AnimeMapping, isCurrent bool) (types.AnimeSeason, error) { -// anime, err := s.jikanClient.GetAnime(mapping.MAL) -// if err != nil { -// return types.AnimeSeason{}, err -// } - -// return types.AnimeSeason{ -// MALID: mapping.MAL, -// Titles: types.AnimeTitles{ -// English: anime.Data.TitleEnglish, -// Japanese: anime.Data.TitleJapanese, -// Romaji: anime.Data.Title, -// Synonyms: anime.Data.TitleSynonyms, -// }, -// Synopsis: anime.Data.Synopsis, -// Type: types.AniSyncType(mapping.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, -// Images: types.AnimeImages{ -// Small: anime.Data.Images.JPG.SmallImageURL, -// Large: anime.Data.Images.JPG.LargeImageURL, -// Original: anime.Data.Images.JPG.ImageURL, -// }, -// 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, -// Current: isCurrent, -// }, nil -// } - -// // Fetch all seasons in parallel -// seasonFunctions := make([]func() (types.AnimeSeason, error), len(*mappings)) - -// for i, mapping := range *mappings { -// mapping := mapping // Capture variable for closure -// isCurrent := mapping.MAL == currentMALID - -// seasonFunctions[i] = func() (types.AnimeSeason, error) { -// return fetchSeason(mapping, isCurrent) -// } -// } - -// // Execute in parallel -// results := concurrency.Parallel(seasonFunctions...) - -// // Extract successful results -// var seasons []types.AnimeSeason -// for _, result := range results { -// if result.Error == nil { -// seasons = append(seasons, result.Value) -// } -// } - -// // Sort seasons chronologically by air date -// if len(seasons) > 1 { -// sortSeasonsByAirDate(&seasons) -// } - -// return seasons -// } diff --git a/tasks/aniupdate.task.go b/tasks/aniupdate.task.go index e6d63a0..864ea05 100644 --- a/tasks/aniupdate.task.go +++ b/tasks/aniupdate.task.go @@ -57,7 +57,7 @@ func AnimeUpdate() error { go func(workerID int) { defer wg.Done() - logger.Debugf("AnimeUpdate", "Started worker #%d", workerID) + logger.Debugf("AnimeUpdate", "Started worker #%d", workerID+1) for job := range jobs { updateAnime(job.series, job.reason) diff --git a/utils/api/aniskip/aniskip.go b/utils/api/aniskip/aniskip.go index eda78f0..d7cc343 100644 --- a/utils/api/aniskip/aniskip.go +++ b/utils/api/aniskip/aniskip.go @@ -126,7 +126,7 @@ func (c *client) makeRequest(ctx context.Context, url string) ([]byte, error) { } func GetSkipTimesForEpisode(malID, episodeNumber int) ([]types.AniskipResult, error) { - url := fmt.Sprintf("%s/skip-times/%d/%d?types=op&types=ed", aniskipBaseURL, malID, episodeNumber) + url := fmt.Sprintf("%s/skip-times/%d/%d?types=op&types=ed&episodeLength=0", aniskipBaseURL, malID, episodeNumber) ctx, cancel := context.WithTimeout(context.Background(), contextTimeout) defer cancel() |
