aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-02-09 16:29:13 +0530
committerBobby <[email protected]>2026-02-09 16:29:13 +0530
commitcd9184421327da59c00d0766452e6082055947f2 (patch)
tree98e618eb029e947d276d367e9b6d96aaa1865a57
parentf382cfae8f0fd2facc7115d1f82dd27ceb1a8258 (diff)
downloadmetachan-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.go10
-rw-r--r--database/migrate.go1
-rw-r--r--entities/anime.go24
-rw-r--r--entities/base.go15
-rw-r--r--entities/episode.go30
-rw-r--r--entities/genre.go4
-rw-r--r--entities/mapping.go4
-rw-r--r--entities/meta.go26
-rw-r--r--entities/persona.go10
-rw-r--r--entities/producer.go4
-rw-r--r--entities/seasons.go14
-rw-r--r--entities/tasks.go6
-rw-r--r--services/anime/helpers.go556
-rw-r--r--services/anime/service.go1078
-rw-r--r--tasks/aniupdate.task.go2
-rw-r--r--utils/api/aniskip/aniskip.go2
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()