diff options
| author | Bobby <[email protected]> | 2026-02-25 10:42:51 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2026-02-25 10:42:51 +0530 |
| commit | c18acfafee26d7d2508848135aa94a524f8dde06 (patch) | |
| tree | e0b4d5e07f872d9252af6d73fb8b108e15a49f91 | |
| parent | 2df69fab61b580b6b329db214ee0025a9d84958d (diff) | |
| download | metachan-c18acfafee26d7d2508848135aa94a524f8dde06.tar.xz metachan-c18acfafee26d7d2508848135aa94a524f8dde06.zip | |
Refactor anime update logic and task management
- Simplified title retrieval in AnimeUpdate and updateAnime functions.
- Updated next airing checks to use new fields directly.
- Removed unnecessary nil checks for titles in various functions.
- Enhanced task management by eliminating redundant LastRun updates.
- Improved dependency handling in triggerDependentTasks for better clarity and performance.
- Streamlined Jikan API response structure by merging related fields.
- Added StopRateLimiters function to rate limiter utilities for better control.
- Refined episode title handling in TMDB and TVDB enrichment functions.
- Introduced Stop method in MultiLimiter for graceful shutdown of rate limiters.
| -rw-r--r-- | database/database.go | 9 | ||||
| -rw-r--r-- | database/migrate.go | 9 | ||||
| -rw-r--r-- | entities/anime.go | 138 | ||||
| -rw-r--r-- | entities/episode.go | 40 | ||||
| -rw-r--r-- | entities/meta.go | 59 | ||||
| -rw-r--r-- | entities/persona.go | 12 | ||||
| -rw-r--r-- | entities/seasons.go | 31 | ||||
| -rw-r--r-- | go.mod | 2 | ||||
| -rw-r--r-- | go.sum | 2 | ||||
| -rw-r--r-- | metachan/main.go | 35 | ||||
| -rw-r--r-- | repositories/anime.go | 30 | ||||
| -rw-r--r-- | repositories/persona.go | 264 | ||||
| -rw-r--r-- | services/anime.go | 477 | ||||
| -rw-r--r-- | tasks/aniupdate.task.go | 34 | ||||
| -rw-r--r-- | tasks/manager.go | 74 | ||||
| -rw-r--r-- | types/jikan.go | 34 | ||||
| -rw-r--r-- | types/tasks.go | 1 | ||||
| -rw-r--r-- | utils/api/aniskip/aniskip.go | 4 | ||||
| -rw-r--r-- | utils/api/jikan/jikan.go | 4 | ||||
| -rw-r--r-- | utils/api/tmdb/tmdb.go | 43 | ||||
| -rw-r--r-- | utils/api/tvdb/tvdb.go | 11 | ||||
| -rw-r--r-- | utils/ratelimit/limiter.go | 6 |
22 files changed, 677 insertions, 642 deletions
diff --git a/database/database.go b/database/database.go index a9215b7..fef6705 100644 --- a/database/database.go +++ b/database/database.go @@ -4,6 +4,7 @@ import ( "metachan/config" "metachan/enums" "metachan/utils/logger" + "time" "gorm.io/driver/mysql" "gorm.io/driver/postgres" @@ -40,6 +41,14 @@ func init() { logger.Fatalf("Database", "Error connecting to database: %v", err) } + sqlDB, err := DB.DB() + if err != nil { + logger.Fatalf("Database", "Failed to get underlying sql.DB: %v", err) + } + sqlDB.SetMaxOpenConns(25) + sqlDB.SetMaxIdleConns(5) + sqlDB.SetConnMaxLifetime(time.Hour) + logger.Successf("Database", "Database connection established successfully") migrate() diff --git a/database/migrate.go b/database/migrate.go index 3fe542e..6bd04ca 100644 --- a/database/migrate.go +++ b/database/migrate.go @@ -10,13 +10,6 @@ func migrate() { &entities.TaskLog{}, &entities.TaskStatus{}, &entities.Mapping{}, - &entities.Title{}, - &entities.Scores{}, - &entities.Date{}, - &entities.AiringStatus{}, - &entities.Broadcast{}, - &entities.Images{}, - &entities.Logos{}, &entities.ExternalURL{}, &entities.SimpleTitle{}, &entities.SimpleImage{}, @@ -25,9 +18,9 @@ func migrate() { &entities.Anime{}, &entities.Episode{}, &entities.EpisodeSkipTime{}, + &entities.StreamInfo{}, &entities.StreamingSource{}, &entities.EpisodeSchedule{}, - &entities.NextEpisode{}, &entities.Season{}, &entities.Character{}, &entities.Person{}, diff --git a/entities/anime.go b/entities/anime.go index 63a1816..db8e4da 100644 --- a/entities/anime.go +++ b/entities/anime.go @@ -4,49 +4,101 @@ import ( "time" ) +type AnimeTitle struct { + English string `json:"english,omitempty"` + Japanese string `json:"japanese,omitempty"` + Romaji string `json:"romaji,omitempty"` + Synonyms []string `gorm:"serializer:json" json:"synonyms,omitempty"` +} + +type AnimeScores struct { + Score float64 `json:"score,omitempty"` + ScoredBy int `json:"scored_by,omitempty"` + Rank int `json:"rank,omitempty"` + Popularity int `json:"popularity,omitempty"` + Members int `json:"members,omitempty"` + Favorites int `json:"favorites,omitempty"` +} + +type AnimeImages struct { + Small string `json:"small,omitempty"` + Large string `json:"large,omitempty"` + Original string `json:"original,omitempty"` +} + +type AnimeLogos struct { + Small string `json:"small,omitempty"` + Medium string `json:"medium,omitempty"` + Large string `json:"large,omitempty"` + XLarge string `json:"xlarge,omitempty"` + Original string `json:"original,omitempty"` +} + +type AnimeBroadcast struct { + Day string `json:"day,omitempty"` + Time string `json:"time,omitempty"` + Timezone string `json:"timezone,omitempty"` + String string `json:"string,omitempty"` +} + +type AnimeAired struct { + From *time.Time `json:"from,omitempty"` + To *time.Time `json:"to,omitempty"` + String string `json:"string,omitempty"` +} + +type AnimeTrailer struct { + YoutubeID string `json:"youtube_id,omitempty"` + URL string `json:"url,omitempty"` + EmbedURL string `json:"embed_url,omitempty"` +} + type Anime struct { BaseModel - MALID int `gorm:"uniqueIndex" json:"id"` - 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"` - Airing bool `json:"airing,omitempty"` - Status string `json:"status,omitempty"` - Duration string `json:"duration,omitempty"` - Color string `json:"color,omitempty"` - Season string `json:"season,omitempty"` - Year int `json:"year,omitempty"` - SubbedCount int `json:"subbed_count,omitempty"` - DubbedCount int `json:"dubbed_count,omitempty"` - TotalEpisodes int `json:"total_episodes,omitempty"` - AiredEpisodes int `json:"aired_episodes,omitempty"` - LastUpdated time.Time `json:"-"` - EnrichedAt *time.Time `json:"-"` - Title *Title `gorm:"foreignKey:TitleID" json:"titles,omitempty"` - Mapping *Mapping `gorm:"foreignKey:MappingID" json:"mappings,omitempty"` - Images *Images `gorm:"foreignKey:ImagesID" json:"images,omitempty"` - Covers *Images `gorm:"foreignKey:CoversID" json:"covers,omitempty"` - Logos *Logos `gorm:"foreignKey:LogosID" json:"logos,omitempty"` - Scores *Scores `gorm:"foreignKey:ScoresID" json:"scores,omitempty"` - AiringStatus *AiringStatus `gorm:"foreignKey:AiringStatusID" json:"airing_status,omitempty"` - 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"` - SeasonNumber int `json:"season_number,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:"-" json:"characters,omitempty"` - Schedule []EpisodeSchedule `gorm:"foreignKey:AnimeID;constraint:OnDelete:CASCADE" json:"airing_schedule,omitempty"` + MALID int `gorm:"uniqueIndex" json:"id"` + MappingID uint `json:"-"` + Synopsis string `gorm:"type:text" json:"synopsis,omitempty"` + Type string `json:"type,omitempty"` + Source string `json:"source,omitempty"` + Airing bool `json:"airing,omitempty"` + Status string `json:"status,omitempty"` + Duration string `json:"duration,omitempty"` + Color string `json:"color,omitempty"` + Season string `json:"season,omitempty"` + Year int `json:"year,omitempty"` + + Rating string `json:"rating,omitempty"` + Background string `gorm:"type:text" json:"background,omitempty"` + + SubbedCount int `json:"subbed_count,omitempty"` + DubbedCount int `json:"dubbed_count,omitempty"` + TotalEpisodes int `json:"total_episodes,omitempty"` + AiredEpisodes int `json:"aired_episodes,omitempty"` + SeasonNumber int `json:"season_number,omitempty"` + NextAiringAt int `json:"next_airing_at,omitempty"` + NextAiringEpisode int `json:"next_airing_episode,omitempty"` + + LastUpdated time.Time `json:"-"` + EnrichedAt *time.Time `json:"-"` + + Title AnimeTitle `gorm:"embedded;embeddedPrefix:title_" json:"titles"` + Scores AnimeScores `gorm:"embedded;embeddedPrefix:score_" json:"scores"` + Images AnimeImages `gorm:"embedded;embeddedPrefix:image_" json:"images"` + Covers AnimeImages `gorm:"embedded;embeddedPrefix:cover_" json:"covers"` + Logos AnimeLogos `gorm:"embedded;embeddedPrefix:logo_" json:"logos"` + Broadcast AnimeBroadcast `gorm:"embedded;embeddedPrefix:broadcast_" json:"broadcast"` + Aired AnimeAired `gorm:"embedded;embeddedPrefix:aired_" json:"aired"` + Trailer AnimeTrailer `gorm:"embedded;embeddedPrefix:trailer_" json:"trailer"` + + Mapping *Mapping `gorm:"foreignKey:MappingID" json:"mappings,omitempty"` + Genres []Genre `gorm:"many2many:anime_genres;" json:"genres,omitempty"` + Themes []Genre `gorm:"many2many:anime_themes;" json:"themes,omitempty"` + Demographics []Genre `gorm:"many2many:anime_demographics;" json:"demographics,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:"-" json:"characters,omitempty"` + Schedule []EpisodeSchedule `gorm:"foreignKey:AnimeID;constraint:OnDelete:CASCADE" json:"airing_schedule,omitempty"` } diff --git a/entities/episode.go b/entities/episode.go index fcf061e..a5c99ef 100644 --- a/entities/episode.go +++ b/entities/episode.go @@ -4,22 +4,27 @@ import ( "time" ) +type EpisodeTitle struct { + English string `json:"english,omitempty"` + Japanese string `json:"japanese,omitempty"` + Romaji string `json:"romaji,omitempty"` +} + type Episode struct { BaseModel - EpisodeID string `gorm:"uniqueIndex;size:32" json:"id"` - AnimeID uint `json:"-"` - TitleID uint `json:"-"` - Description string `gorm:"type:text" json:"description,omitempty"` - Aired string `json:"aired,omitempty"` - Score float64 `json:"score,omitempty"` - Filler bool `json:"filler,omitempty"` - Recap bool `json:"recap,omitempty"` - ForumURL string `json:"forum_url,omitempty"` - URL string `json:"url,omitempty"` - ThumbnailURL string `json:"thumbnail_url,omitempty"` - EpisodeNumber int `json:"episode_number,omitempty"` - EpisodeLength float64 `json:"episode_length,omitempty"` - Title *Title `gorm:"foreignKey:TitleID" json:"titles,omitempty"` + EpisodeID string `gorm:"uniqueIndex;size:32" json:"id"` + AnimeID uint `gorm:"index" json:"-"` + Description string `gorm:"type:text" json:"description,omitempty"` + Aired string `json:"aired,omitempty"` + Score float64 `json:"score,omitempty"` + Filler bool `json:"filler,omitempty"` + Recap bool `json:"recap,omitempty"` + ForumURL string `json:"forum_url,omitempty"` + URL string `json:"url,omitempty"` + ThumbnailURL string `json:"thumbnail_url,omitempty"` + EpisodeNumber int `json:"episode_number,omitempty"` + EpisodeLength float64 `json:"episode_length,omitempty"` + Title EpisodeTitle `gorm:"embedded;embeddedPrefix:title_" json:"titles"` StreamInfo *StreamInfo `gorm:"foreignKey:EpisodeID;references:EpisodeID" json:"streaming,omitempty"` SkipTimes []EpisodeSkipTime `gorm:"foreignKey:EpisodeID;references:EpisodeID" json:"skip_times,omitempty"` } @@ -40,13 +45,6 @@ type EpisodeSchedule struct { IsNext bool `gorm:"index" json:"is_next,omitempty"` } -type NextEpisode struct { - BaseModel - AnimeID uint `json:"-"` - AiringAt int `json:"airing_at,omitempty"` - Episode int `json:"episode,omitempty"` -} - type StreamInfo struct { BaseModel EpisodeID string `gorm:"uniqueIndex:idx_episode_streaming;size:32" json:"-"` diff --git a/entities/meta.go b/entities/meta.go index 2395ddd..7690c45 100644 --- a/entities/meta.go +++ b/entities/meta.go @@ -1,64 +1,5 @@ package entities -type Title struct { - BaseModel - English string `json:"english,omitempty"` - Japanese string `json:"japanese,omitempty"` - Romaji string `json:"romaji,omitempty"` - Synonyms []string `gorm:"serializer:json" json:"synonyms,omitempty"` -} - -type Scores struct { - BaseModel - Score float64 `json:"score,omitempty"` - ScoredBy int `json:"scored_by,omitempty"` - Rank int `json:"rank,omitempty"` - Popularity int `json:"popularity,omitempty"` - Members int `json:"members,omitempty"` - Favorites int `json:"favorites,omitempty"` -} - -type Date struct { - BaseModel - Day int `json:"day,omitempty"` - Month int `json:"month,omitempty"` - Year int `json:"year,omitempty"` - String string `json:"string,omitempty"` -} - -type AiringStatus struct { - 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 { - BaseModel - Day string `json:"day,omitempty"` - Time string `json:"time,omitempty"` - Timezone string `json:"timezone,omitempty"` - String string `json:"string,omitempty"` -} - -type Images struct { - BaseModel - Small string `json:"small,omitempty"` - Large string `json:"large,omitempty"` - Original string `json:"original,omitempty"` -} - -type Logos struct { - BaseModel - Small string `json:"small,omitempty"` - Medium string `json:"medium,omitempty"` - Large string `json:"large,omitempty"` - XLarge string `json:"xlarge,omitempty"` - Original string `json:"original,omitempty"` -} - type ExternalURL struct { BaseModel Name string `json:"name,omitempty"` diff --git a/entities/persona.go b/entities/persona.go index 9390b96..c523361 100644 --- a/entities/persona.go +++ b/entities/persona.go @@ -53,7 +53,7 @@ type PersonCharacterEntry struct { } type PersonVoiceRole struct { - PersonID uint `gorm:"primaryKey" json:"-"` + PersonID uint `gorm:"primaryKey;index" json:"-"` AnimeMALID int `gorm:"primaryKey" json:"anime_mal_id,omitempty"` CharacterMALID int `gorm:"primaryKey" json:"character_mal_id,omitempty"` Role string `json:"role,omitempty"` @@ -66,7 +66,7 @@ type PersonVoiceRole struct { } type PersonAnimeCredit struct { - PersonID uint `gorm:"primaryKey" json:"-"` + PersonID uint `gorm:"primaryKey;index" json:"-"` AnimeMALID int `gorm:"primaryKey" json:"mal_id,omitempty"` Position string `json:"position,omitempty"` AnimeTitle string `json:"title,omitempty"` @@ -75,7 +75,7 @@ type PersonAnimeCredit struct { } type PersonMangaCredit struct { - PersonID uint `gorm:"primaryKey" json:"-"` + PersonID uint `gorm:"primaryKey;index" json:"-"` MangaMALID int `gorm:"primaryKey" json:"mal_id,omitempty"` Position string `json:"position,omitempty"` MangaTitle string `json:"title,omitempty"` @@ -84,13 +84,13 @@ type PersonMangaCredit struct { } type AnimeCharacter struct { - AnimeID uint `gorm:"primaryKey"` - CharacterID uint `gorm:"primaryKey"` + AnimeID uint `gorm:"primaryKey;index"` + CharacterID uint `gorm:"primaryKey"` Role string } type CharacterVoiceActor struct { - CharacterID uint `gorm:"primaryKey" json:"-"` + CharacterID uint `gorm:"primaryKey;index" json:"-"` PersonID uint `gorm:"primaryKey" json:"-"` Language string `json:"language,omitempty"` Person *Person `gorm:"foreignKey:PersonID;references:ID" json:"person,omitempty"` diff --git a/entities/seasons.go b/entities/seasons.go index 54b6367..158462d 100644 --- a/entities/seasons.go +++ b/entities/seasons.go @@ -2,24 +2,15 @@ package entities type Season struct { BaseModel - ParentAnimeID uint `json:"-"` - MALID int `json:"mal_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"` - Airing bool `json:"airing,omitempty"` - Status string `json:"status,omitempty"` - Duration string `json:"duration,omitempty"` - Season string `json:"season,omitempty"` - Year int `json:"year,omitempty"` - Current bool `json:"current,omitempty"` - Title *Title `gorm:"foreignKey:TitleID" json:"titles,omitempty"` - SeasonNumber int `json:"season_number,omitempty"` - Images *Images `gorm:"foreignKey:ImagesID" json:"images,omitempty"` - Scores *Scores `gorm:"foreignKey:ScoresID" json:"scores,omitempty"` - AiringStatus *AiringStatus `gorm:"foreignKey:AiringStatusID" json:"airing_status,omitempty"` + ParentAnimeID uint `json:"-"` + MALID int `json:"mal_id,omitempty"` + SeasonNumber int `json:"season_number,omitempty"` + Current bool `json:"current,omitempty"` + TitleEnglish string `json:"title_english,omitempty"` + TitleRomaji string `json:"title_romaji,omitempty"` + ImageOriginal string `json:"image_original,omitempty"` + Year int `json:"year,omitempty"` + SeasonName string `json:"season,omitempty"` + Type string `json:"type,omitempty"` + Status string `json:"status,omitempty"` } @@ -37,7 +37,7 @@ require ( github.com/valyala/tcplisten v1.0.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/crypto v0.18.0 // indirect - golang.org/x/sync v0.1.0 // indirect + golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.14.0 // indirect ) @@ -139,6 +139,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/metachan/main.go b/metachan/main.go index 85611fc..156fe52 100644 --- a/metachan/main.go +++ b/metachan/main.go @@ -3,10 +3,16 @@ package main import ( "fmt" "metachan/config" + "metachan/database" "metachan/middleware" "metachan/router" "metachan/tasks" + "metachan/utils/api/aniskip" + "metachan/utils/api/jikan" "metachan/utils/logger" + "os" + "os/signal" + "syscall" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" @@ -31,10 +37,31 @@ func main() { middleware.Initialize(app) router.Initialize(app) - // Start the server - if err := app.Listen(fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port)); err != nil { - logger.Fatalf("Main", "Failed to the start the server on %s:%d: %v", config.Server.Host, config.Server.Port, err) - } + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + + go func() { + if err := app.Listen(fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port)); err != nil { + logger.Fatalf("Main", "Failed to start the server on %s:%d: %v", config.Server.Host, config.Server.Port, err) + } + }() logger.Successf("Main", "Server started on %s:%d", config.Server.Host, config.Server.Port) + + <-quit + logger.Infof("Main", "Shutting down gracefully...") + + if err := app.Shutdown(); err != nil { + logger.Errorf("Main", "Error during server shutdown: %v", err) + } + + tasks.GlobalTaskManager.StopAllTasks() + jikan.StopRateLimiters() + aniskip.StopRateLimiters() + + if sqlDB, err := database.DB.DB(); err == nil { + sqlDB.Close() + } + + logger.Successf("Main", "Shutdown complete") } diff --git a/repositories/anime.go b/repositories/anime.go index 4778f72..ae823c2 100644 --- a/repositories/anime.go +++ b/repositories/anime.go @@ -23,17 +23,9 @@ func GetAnime[T idType](maptype enums.MappingType, id T) (entities.Anime, error) result := DB. Preload("Mapping"). - Preload("Title"). - Preload("Images"). - Preload("Covers"). - Preload("Logos"). - Preload("Scores"). - Preload("AiringStatus"). - Preload("AiringStatus.From"). - Preload("AiringStatus.To"). - Preload("Broadcast"). - Preload("NextAiring"). Preload("Genres"). + Preload("Themes"). + Preload("Demographics"). Preload("Producers"). Preload("Producers.Image"). Preload("Producers.Titles"). @@ -47,19 +39,12 @@ func GetAnime[T idType](maptype enums.MappingType, id T) (entities.Anime, error) Preload("Licensors.Titles"). Preload("Licensors.ExternalURLs"). Preload("Episodes"). - Preload("Episodes.Title"). Preload("Episodes.SkipTimes"). Preload("Episodes.StreamInfo"). Preload("Episodes.StreamInfo.SubSources"). Preload("Episodes.StreamInfo.DubSources"). Preload("Schedule"). Preload("Seasons"). - Preload("Seasons.Title"). - Preload("Seasons.Images"). - Preload("Seasons.Scores"). - Preload("Seasons.AiringStatus"). - Preload("Seasons.AiringStatus.From"). - Preload("Seasons.AiringStatus.To"). Where("mapping_id = ?", mapping.ID). First(&anime) @@ -110,12 +95,7 @@ func SaveAnimeEpisodes(animeID uint, episodes []entities.Episode) error { var existing entities.Episode if DB.Where("episode_id = ?", ep.EpisodeID).First(&existing).Error == nil { ep.ID = existing.ID - ep.TitleID = existing.TitleID - DB.Model(ep).Omit("SkipTimes", "StreamInfo", "Title").Updates(ep) - if ep.Title != nil && existing.TitleID != 0 { - ep.Title.ID = existing.TitleID - DB.Save(ep.Title) - } + DB.Model(ep).Omit("SkipTimes", "StreamInfo").Updates(ep) } else { DB.Session(&gorm.Session{FullSaveAssociations: true}). Omit("SkipTimes", "StreamInfo"). @@ -158,7 +138,6 @@ func GetAnimeEpisode[T idType](maptype enums.MappingType, id T, episodeID string var episode entities.Episode result := DB. - Preload("Title"). Preload("SkipTimes"). Preload("StreamInfo"). Preload("StreamInfo.SubSources"). @@ -192,7 +171,6 @@ func GetAnimeEpisodes[T idType](maptype enums.MappingType, id T) ([]entities.Epi var episodes []entities.Episode result := DB. - Preload("Title"). Preload("SkipTimes"). Preload("StreamInfo"). Preload("StreamInfo.SubSources"). @@ -240,9 +218,7 @@ func GetAiringAnime() ([]entities.Anime, error) { result := DB. Where("airing = ?", true). - Preload("NextAiring"). Preload("Schedule"). - Preload("Title"). Find(&anime) if result.Error != nil { diff --git a/repositories/persona.go b/repositories/persona.go index 1cb808c..1da6d29 100644 --- a/repositories/persona.go +++ b/repositories/persona.go @@ -36,43 +36,45 @@ func UpdateCharacterDetails(malID int, name, nameKanji, url, imageURL, about str return err } - DB.Where("character_id = ?", char.ID).Delete(&entities.CharacterVoiceActor{}) - for _, cva := range voiceActors { - va := cva.Person - if va == nil { - continue - } + return DB.Transaction(func(tx *gorm.DB) error { + tx.Where("character_id = ?", char.ID).Delete(&entities.CharacterVoiceActor{}) + for _, cva := range voiceActors { + va := cva.Person + if va == nil { + continue + } - DB.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "mal_id"}}, - DoUpdates: clause.AssignmentColumns([]string{"url", "image", "name"}), - }).Create(va) - if va.ID == 0 { - DB.Where("mal_id = ?", va.MALID).First(va) - } + tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "mal_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"url", "image", "name"}), + }).Create(va) + if va.ID == 0 { + tx.Where("mal_id = ?", va.MALID).First(va) + } - DB.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "character_id"}, {Name: "person_id"}}, - DoUpdates: clause.AssignmentColumns([]string{"language"}), - }).Create(&entities.CharacterVoiceActor{ - CharacterID: char.ID, - PersonID: va.ID, - Language: cva.Language, - }) - } + tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "character_id"}, {Name: "person_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"language"}), + }).Create(&entities.CharacterVoiceActor{ + CharacterID: char.ID, + PersonID: va.ID, + Language: cva.Language, + }) + } - DB.Where("character_id = ?", char.ID).Delete(&entities.CharacterAnimeAppearance{}) - for i := range animeAppearances { - animeAppearances[i].CharacterID = char.ID - } - if len(animeAppearances) > 0 { - DB.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "character_id"}, {Name: "anime_mal_id"}}, - DoUpdates: clause.AssignmentColumns([]string{"title", "url", "image_url", "role"}), - }).Create(&animeAppearances) - } + tx.Where("character_id = ?", char.ID).Delete(&entities.CharacterAnimeAppearance{}) + for i := range animeAppearances { + animeAppearances[i].CharacterID = char.ID + } + if len(animeAppearances) > 0 { + tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "character_id"}, {Name: "anime_mal_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"title", "url", "image_url", "role"}), + }).Create(&animeAppearances) + } - return nil + return nil + }) } func GetAnimeCharacters[T idType](maptype enums.MappingType, id T) ([]entities.Character, error) { @@ -98,15 +100,31 @@ func GetAnimeCharacters[T idType](maptype enums.MappingType, id T) ([]entities.C Where("anime_id = ?", anime.ID). Scan(&rows) + if len(rows) == 0 { + return []entities.Character{}, nil + } + + charIDs := make([]uint, len(rows)) + roleMap := make(map[uint]string, len(rows)) + for i, row := range rows { + charIDs[i] = row.CharacterID + roleMap[row.CharacterID] = row.Role + } + var characters []entities.Character - for _, row := range rows { - var char entities.Character - if err := DB.First(&char, row.CharacterID).Error; err != nil { - continue - } - DB.Preload("Person").Where("character_id = ?", char.ID).Find(&char.VoiceActors) - char.Role = row.Role - characters = append(characters, char) + DB.Where("id IN ?", charIDs).Find(&characters) + + var voiceActors []entities.CharacterVoiceActor + DB.Preload("Person").Where("character_id IN ?", charIDs).Find(&voiceActors) + + voiceActorsByCharacterID := make(map[uint][]entities.CharacterVoiceActor) + for _, voiceActor := range voiceActors { + voiceActorsByCharacterID[voiceActor.CharacterID] = append(voiceActorsByCharacterID[voiceActor.CharacterID], voiceActor) + } + + for i := range characters { + characters[i].Role = roleMap[characters[i].ID] + characters[i].VoiceActors = voiceActorsByCharacterID[characters[i].ID] } return characters, nil @@ -159,17 +177,34 @@ func loadAnimeCharacters(anime *entities.Anime) { Where("anime_id = ?", anime.ID). Scan(&rows) - for _, row := range rows { - var char entities.Character - if err := DB.First(&char, row.CharacterID).Error; err != nil { - continue - } - DB.Preload("Person"). - Where("character_id = ?", char.ID). - Find(&char.VoiceActors) - char.Role = row.Role - anime.Characters = append(anime.Characters, char) + if len(rows) == 0 { + return } + + charIDs := make([]uint, len(rows)) + roleMap := make(map[uint]string, len(rows)) + for i, row := range rows { + charIDs[i] = row.CharacterID + roleMap[row.CharacterID] = row.Role + } + + var characters []entities.Character + DB.Where("id IN ?", charIDs).Find(&characters) + + var voiceActors []entities.CharacterVoiceActor + DB.Preload("Person").Where("character_id IN ?", charIDs).Find(&voiceActors) + + voiceActorsByCharacterID := make(map[uint][]entities.CharacterVoiceActor) + for _, voiceActor := range voiceActors { + voiceActorsByCharacterID[voiceActor.CharacterID] = append(voiceActorsByCharacterID[voiceActor.CharacterID], voiceActor) + } + + for i := range characters { + characters[i].Role = roleMap[characters[i].ID] + characters[i].VoiceActors = voiceActorsByCharacterID[characters[i].ID] + } + + anime.Characters = characters } func SaveAnimeCharacters(animeID uint, characters []entities.Character) error { @@ -279,40 +314,48 @@ func UpdatePersonDetails( return err } - DB.Where("person_id = ?", p.ID).Delete(&entities.PersonVoiceRole{}) - for i := range voiceRoles { - voiceRoles[i].PersonID = p.ID - } - if len(voiceRoles) > 0 { - DB.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "person_id"}, {Name: "anime_mal_id"}, {Name: "character_mal_id"}}, - DoUpdates: clause.AssignmentColumns([]string{"role", "anime_title", "anime_url", "anime_image_url", "character_name", "character_url", "character_image_url"}), - }).Create(&voiceRoles) - } + return DB.Transaction(func(tx *gorm.DB) error { + tx.Where("person_id = ?", p.ID).Delete(&entities.PersonVoiceRole{}) + for i := range voiceRoles { + voiceRoles[i].PersonID = p.ID + } + if len(voiceRoles) > 0 { + if err := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "person_id"}, {Name: "anime_mal_id"}, {Name: "character_mal_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"role", "anime_title", "anime_url", "anime_image_url", "character_name", "character_url", "character_image_url"}), + }).Create(&voiceRoles).Error; err != nil { + return err + } + } - DB.Where("person_id = ?", p.ID).Delete(&entities.PersonAnimeCredit{}) - for i := range animeCredits { - animeCredits[i].PersonID = p.ID - } - if len(animeCredits) > 0 { - DB.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "person_id"}, {Name: "anime_mal_id"}}, - DoUpdates: clause.AssignmentColumns([]string{"position", "anime_title", "anime_url", "anime_image_url"}), - }).Create(&animeCredits) - } + tx.Where("person_id = ?", p.ID).Delete(&entities.PersonAnimeCredit{}) + for i := range animeCredits { + animeCredits[i].PersonID = p.ID + } + if len(animeCredits) > 0 { + if err := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "person_id"}, {Name: "anime_mal_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"position", "anime_title", "anime_url", "anime_image_url"}), + }).Create(&animeCredits).Error; err != nil { + return err + } + } - DB.Where("person_id = ?", p.ID).Delete(&entities.PersonMangaCredit{}) - for i := range mangaCredits { - mangaCredits[i].PersonID = p.ID - } - if len(mangaCredits) > 0 { - DB.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "person_id"}, {Name: "manga_mal_id"}}, - DoUpdates: clause.AssignmentColumns([]string{"position", "manga_title", "manga_url", "manga_image_url"}), - }).Create(&mangaCredits) - } + tx.Where("person_id = ?", p.ID).Delete(&entities.PersonMangaCredit{}) + for i := range mangaCredits { + mangaCredits[i].PersonID = p.ID + } + if len(mangaCredits) > 0 { + if err := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "person_id"}, {Name: "manga_mal_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"position", "manga_title", "manga_url", "manga_image_url"}), + }).Create(&mangaCredits).Error; err != nil { + return err + } + } - return nil + return nil + }) } func SetPersonEnriched(malID int) error { @@ -334,46 +377,49 @@ func GetAnimePeople[T idType](maptype enums.MappingType, id T) ([]entities.Perso return nil, errors.New("anime not found") } - var charRows []struct { - CharacterID uint - } + var charIDs []uint DB.Table("anime_characters"). Select("character_id"). Where("anime_id = ?", anime.ID). - Scan(&charRows) + Pluck("character_id", &charIDs) + + if len(charIDs) == 0 { + return []entities.Person{}, nil + } + + var characters []entities.Character + DB.Where("id IN ?", charIDs).Find(&characters) + + characterByID := make(map[uint]*entities.Character, len(characters)) + for i := range characters { + characterByID[characters[i].ID] = &characters[i] + } + + var characterVoiceActors []entities.CharacterVoiceActor + DB.Preload("Person").Where("character_id IN ?", charIDs).Find(&characterVoiceActors) personMap := make(map[uint]*entities.Person) - personChars := make(map[uint][]entities.PersonCharacterEntry) + personCharacters := make(map[uint][]entities.PersonCharacterEntry) - for _, row := range charRows { - var char entities.Character - if err := DB.First(&char, row.CharacterID).Error; err != nil { + for _, voiceActorEntry := range characterVoiceActors { + if voiceActorEntry.Person == nil { continue } - - var cvas []entities.CharacterVoiceActor - DB.Preload("Person").Where("character_id = ?", char.ID).Find(&cvas) - - for _, cva := range cvas { - if cva.Person == nil { - continue - } - pID := cva.PersonID - if _, exists := personMap[pID]; !exists { - personMap[pID] = cva.Person - } - charCopy := char - personChars[pID] = append(personChars[pID], entities.PersonCharacterEntry{ - Character: &charCopy, - Language: cva.Language, + if _, exists := personMap[voiceActorEntry.PersonID]; !exists { + personMap[voiceActorEntry.PersonID] = voiceActorEntry.Person + } + if character, ok := characterByID[voiceActorEntry.CharacterID]; ok { + personCharacters[voiceActorEntry.PersonID] = append(personCharacters[voiceActorEntry.PersonID], entities.PersonCharacterEntry{ + Character: character, + Language: voiceActorEntry.Language, }) } } result := make([]entities.Person, 0, len(personMap)) - for pID, p := range personMap { - p.Characters = personChars[pID] - result = append(result, *p) + for personID, person := range personMap { + person.Characters = personCharacters[personID] + result = append(result, *person) } return result, nil } diff --git a/services/anime.go b/services/anime.go index 787943b..7a21300 100644 --- a/services/anime.go +++ b/services/anime.go @@ -1,6 +1,7 @@ package services import ( + "context" "crypto/md5" "fmt" "metachan/entities" @@ -17,14 +18,31 @@ import ( "metachan/utils/logger" "strings" "time" + + "golang.org/x/sync/errgroup" + "golang.org/x/sync/singleflight" ) +var flightGroup singleflight.Group + func GetAnime(mapping *entities.Mapping) (*entities.Anime, error) { if mapping == nil { logger.Errorf("AnimeService", "Mapping is nil") return nil, fmt.Errorf("mapping is nil") } + key := fmt.Sprintf("anime:%d", mapping.MAL) + result, err, _ := flightGroup.Do(key, func() (interface{}, error) { + return getAnimeInternal(mapping) + }) + + if err != nil { + return nil, err + } + return result.(*entities.Anime), nil +} + +func getAnimeInternal(mapping *entities.Mapping) (*entities.Anime, error) { malID := mapping.MAL logger.Infof("AnimeService", "Fetching anime data for MAL ID: %d", malID) @@ -70,26 +88,71 @@ func fetchAnime(mapping *entities.Mapping, existing *entities.Anime) (*entities. } } - jikanAnime, err := jikan.GetAnimeByMALID(malID) - if err != nil { - logger.Errorf("AnimeService", "Failed to fetch anime from Jikan: %v", err) - return nil, fmt.Errorf("failed to fetch anime from Jikan: %w", err) - } + var jikanAnime *types.JikanAnimeResponse + var jikanEpisodes *types.JikanAnimeEpisodeResponse + var jikanCharacters *types.JikanAnimeCharacterResponse + var anilistData *types.AnilistAnimeResponse + var malSyncData *types.MalsyncAnimeResponse - jikanEpisodes, err := jikan.GetAnimeEpisodesByMALID(malID) - if err != nil { - logger.Errorf("AnimeService", "Failed to fetch episodes from Jikan: %v", err) - return nil, fmt.Errorf("failed to fetch episodes from Jikan: %w", err) + fetchGroup, _ := errgroup.WithContext(context.Background()) + + fetchGroup.Go(func() error { + var err error + jikanAnime, err = jikan.GetAnimeByMALID(malID) + if err != nil { + return fmt.Errorf("failed to fetch anime from Jikan: %w", err) + } + return nil + }) + + fetchGroup.Go(func() error { + var err error + jikanEpisodes, err = jikan.GetAnimeEpisodesByMALID(malID) + if err != nil { + return fmt.Errorf("failed to fetch episodes from Jikan: %w", err) + } + return nil + }) + + fetchGroup.Go(func() error { + var err error + jikanCharacters, err = jikan.GetAnimeCharactersByMALID(malID) + if err != nil { + logger.Warnf("AnimeService", "Failed to fetch characters from Jikan: %v", err) + } + return nil + }) + + if mapping.Anilist > 0 { + fetchGroup.Go(func() error { + var err error + anilistData, err = anilist.GetAnimeByAnilistID(mapping.Anilist) + if err != nil { + logger.Warnf("AnimeService", "Failed to fetch Anilist data: %v", err) + } + return nil + }) } - jikanCharacters, err := jikan.GetAnimeCharactersByMALID(malID) - if err != nil { - logger.Warnf("AnimeService", "Failed to fetch characters from Jikan: %v", err) + fetchGroup.Go(func() error { + var err error + malSyncData, err = malsync.GetAnimeByMALID(malID) + if err != nil { + logger.Warnf("AnimeService", "Failed to fetch MALsync data: %v", err) + } + return nil + }) + + if err := fetchGroup.Wait(); err != nil { + logger.Errorf("AnimeService", "Failed to fetch anime data: %v", err) + return nil, err } anime.Episodes = nil anime.Characters = nil anime.Genres = nil + anime.Themes = nil + anime.Demographics = nil anime.Producers = nil anime.Studios = nil anime.Licensors = nil @@ -97,19 +160,11 @@ func fetchAnime(mapping *entities.Mapping, existing *entities.Anime) (*entities. applyJikanData(anime, jikanAnime, jikanEpisodes, jikanCharacters) - if mapping.Anilist > 0 { - anilistData, err := anilist.GetAnimeByAnilistID(mapping.Anilist) - if err != nil { - logger.Warnf("AnimeService", "Failed to fetch Anilist data: %v", err) - } else { - applyAnilistData(anime, anilistData) - } + if anilistData != nil { + applyAnilistData(anime, anilistData) } - malSyncData, err := malsync.GetAnimeByMALID(malID) - if err != nil { - logger.Warnf("AnimeService", "Failed to fetch MALsync data: %v", err) - } else { + if malSyncData != nil { applyMALsyncData(anime, malSyncData) } @@ -187,17 +242,17 @@ func applyJikanData(anime *entities.Anime, jikanAnime *types.JikanAnimeResponse, anime.Duration = jikanAnime.Data.Duration anime.Season = jikanAnime.Data.Season anime.Year = jikanAnime.Data.Year + anime.Rating = jikanAnime.Data.Rating + anime.Background = jikanAnime.Data.Background - if jikanAnime.Data.Title != "" || jikanAnime.Data.TitleEnglish != "" || jikanAnime.Data.TitleJapanese != "" { - anime.Title = &entities.Title{ - Romaji: jikanAnime.Data.Title, - English: jikanAnime.Data.TitleEnglish, - Japanese: jikanAnime.Data.TitleJapanese, - Synonyms: jikanAnime.Data.TitleSynonyms, - } + anime.Title = entities.AnimeTitle{ + Romaji: jikanAnime.Data.Title, + English: jikanAnime.Data.TitleEnglish, + Japanese: jikanAnime.Data.TitleJapanese, + Synonyms: jikanAnime.Data.TitleSynonyms, } - anime.Scores = &entities.Scores{ + anime.Scores = entities.AnimeScores{ Score: jikanAnime.Data.Score, ScoredBy: jikanAnime.Data.ScoredBy, Rank: jikanAnime.Data.Rank, @@ -206,90 +261,100 @@ func applyJikanData(anime *entities.Anime, jikanAnime *types.JikanAnimeResponse, Favorites: jikanAnime.Data.Favorites, } - if jikanAnime.Data.Images.JPG.ImageURL != "" { - anime.Images = &entities.Images{ - Small: jikanAnime.Data.Images.JPG.SmallImageURL, - Large: jikanAnime.Data.Images.JPG.LargeImageURL, - Original: jikanAnime.Data.Images.JPG.ImageURL, - } + anime.Images = entities.AnimeImages{ + Small: jikanAnime.Data.Images.JPG.SmallImageURL, + Large: jikanAnime.Data.Images.JPG.LargeImageURL, + Original: jikanAnime.Data.Images.JPG.ImageURL, } - if jikanAnime.Data.Aired.From != "" || jikanAnime.Data.Aired.To != "" { - anime.AiringStatus = &entities.AiringStatus{ - String: jikanAnime.Data.Aired.String, - } - if jikanAnime.Data.Aired.Prop.From.Year > 0 { - anime.AiringStatus.From = &entities.Date{ - Day: jikanAnime.Data.Aired.Prop.From.Day, - Month: jikanAnime.Data.Aired.Prop.From.Month, - Year: jikanAnime.Data.Aired.Prop.From.Year, - String: jikanAnime.Data.Aired.From, - } + anime.Aired = entities.AnimeAired{ + String: jikanAnime.Data.Aired.String, + } + if jikanAnime.Data.Aired.From != "" { + if parsedTime, err := time.Parse(time.RFC3339, jikanAnime.Data.Aired.From); err == nil { + anime.Aired.From = &parsedTime } - if jikanAnime.Data.Aired.Prop.To.Year > 0 { - anime.AiringStatus.To = &entities.Date{ - Day: jikanAnime.Data.Aired.Prop.To.Day, - Month: jikanAnime.Data.Aired.Prop.To.Month, - Year: jikanAnime.Data.Aired.Prop.To.Year, - String: jikanAnime.Data.Aired.To, - } + } + if jikanAnime.Data.Aired.To != "" { + if parsedTime, err := time.Parse(time.RFC3339, jikanAnime.Data.Aired.To); err == nil { + anime.Aired.To = &parsedTime } } - if jikanAnime.Data.Broadcast.Day != "" { - anime.Broadcast = &entities.Broadcast{ - Day: jikanAnime.Data.Broadcast.Day, - Time: jikanAnime.Data.Broadcast.Time, - Timezone: jikanAnime.Data.Broadcast.Timezone, - String: jikanAnime.Data.Broadcast.String, - } + anime.Broadcast = entities.AnimeBroadcast{ + Day: jikanAnime.Data.Broadcast.Day, + Time: jikanAnime.Data.Broadcast.Time, + Timezone: jikanAnime.Data.Broadcast.Timezone, + String: jikanAnime.Data.Broadcast.String, } - for _, jg := range jikanAnime.Data.Genres { + anime.Trailer = entities.AnimeTrailer{ + YoutubeID: jikanAnime.Data.Trailer.YoutubeID, + URL: jikanAnime.Data.Trailer.URL, + EmbedURL: jikanAnime.Data.Trailer.EmbedURL, + } + + for _, genreEntry := range jikanAnime.Data.Genres { anime.Genres = append(anime.Genres, entities.Genre{ - GenreID: jg.MALID, - Name: jg.Name, - URL: jg.URL, + GenreID: genreEntry.MALID, + Name: genreEntry.Name, + URL: genreEntry.URL, }) } - for _, jg := range jikanAnime.Data.ExplicitGenres { + for _, genreEntry := range jikanAnime.Data.ExplicitGenres { anime.Genres = append(anime.Genres, entities.Genre{ - GenreID: jg.MALID, - Name: jg.Name, - URL: jg.URL, + GenreID: genreEntry.MALID, + Name: genreEntry.Name, + URL: genreEntry.URL, + }) + } + + for _, themeEntry := range jikanAnime.Data.Themes { + anime.Themes = append(anime.Themes, entities.Genre{ + GenreID: themeEntry.MALID, + Name: themeEntry.Name, + URL: themeEntry.URL, + }) + } + + for _, demographicEntry := range jikanAnime.Data.Demographics { + anime.Demographics = append(anime.Demographics, entities.Genre{ + GenreID: demographicEntry.MALID, + Name: demographicEntry.Name, + URL: demographicEntry.URL, }) } - for _, jp := range jikanAnime.Data.Producers { + for _, producerEntry := range jikanAnime.Data.Producers { producer := entities.Producer{ - MALID: jp.MALID, - URL: jp.URL, + MALID: producerEntry.MALID, + URL: producerEntry.URL, } - if jp.Name != "" { - producer.Titles = []entities.SimpleTitle{{Title: jp.Name, Type: "Default"}} + if producerEntry.Name != "" { + producer.Titles = []entities.SimpleTitle{{Title: producerEntry.Name, Type: "Default"}} } anime.Producers = append(anime.Producers, producer) } - for _, js := range jikanAnime.Data.Studios { + for _, studioEntry := range jikanAnime.Data.Studios { studio := entities.Producer{ - MALID: js.MALID, - URL: js.URL, + MALID: studioEntry.MALID, + URL: studioEntry.URL, } - if js.Name != "" { - studio.Titles = []entities.SimpleTitle{{Title: js.Name, Type: "Default"}} + if studioEntry.Name != "" { + studio.Titles = []entities.SimpleTitle{{Title: studioEntry.Name, Type: "Default"}} } anime.Studios = append(anime.Studios, studio) } - for _, jl := range jikanAnime.Data.Licensors { + for _, licensorEntry := range jikanAnime.Data.Licensors { licensor := entities.Producer{ - MALID: jl.MALID, - URL: jl.URL, + MALID: licensorEntry.MALID, + URL: licensorEntry.URL, } - if jl.Name != "" { - licensor.Titles = []entities.SimpleTitle{{Title: jl.Name, Type: "Default"}} + if licensorEntry.Name != "" { + licensor.Titles = []entities.SimpleTitle{{Title: licensorEntry.Name, Type: "Default"}} } anime.Licensors = append(anime.Licensors, licensor) } @@ -297,54 +362,51 @@ func applyJikanData(anime *entities.Anime, jikanAnime *types.JikanAnimeResponse, anime.TotalEpisodes = jikanAnime.Data.Episodes anime.AiredEpisodes = len(jikanEpisodes.Data) - for _, je := range jikanEpisodes.Data { + for _, jikanEpisode := range jikanEpisodes.Data { episode := entities.Episode{ - EpisodeNumber: je.MALID, - URL: je.URL, - Aired: je.Aired, - Score: je.Score, - Filler: je.Filler, - Recap: je.Recap, - ForumURL: je.ForumURL, - } - - if je.Title != "" || je.TitleJapanese != "" || je.TitleRomaji != "" { - episode.Title = &entities.Title{ - English: je.Title, - Japanese: je.TitleJapanese, - Romaji: je.TitleRomaji, - } - } - - titleForID := je.Title + EpisodeNumber: jikanEpisode.MALID, + URL: jikanEpisode.URL, + Aired: jikanEpisode.Aired, + Score: jikanEpisode.Score, + Filler: jikanEpisode.Filler, + Recap: jikanEpisode.Recap, + ForumURL: jikanEpisode.ForumURL, + Title: entities.EpisodeTitle{ + English: jikanEpisode.Title, + Japanese: jikanEpisode.TitleJapanese, + Romaji: jikanEpisode.TitleRomaji, + }, + } + + titleForID := jikanEpisode.Title if titleForID == "" { - titleForID = je.TitleRomaji + titleForID = jikanEpisode.TitleRomaji } - episode.EpisodeID = generateEpisodeID(anime.MALID, je.MALID, titleForID) + episode.EpisodeID = generateEpisodeID(anime.MALID, jikanEpisode.MALID, titleForID) anime.Episodes = append(anime.Episodes, episode) } if jikanCharacters != nil { - for _, jc := range jikanCharacters.Data { - char := entities.Character{ - MALID: jc.Character.MALID, - Name: jc.Character.Name, - URL: jc.Character.URL, - ImageURL: jc.Character.Images.JPG.ImageURL, - Role: jc.Role, + for _, jikanCharacter := range jikanCharacters.Data { + character := entities.Character{ + MALID: jikanCharacter.Character.MALID, + Name: jikanCharacter.Character.Name, + URL: jikanCharacter.Character.URL, + ImageURL: jikanCharacter.Character.Images.JPG.ImageURL, + Role: jikanCharacter.Role, } - for _, va := range jc.VoiceActors { - char.VoiceActors = append(char.VoiceActors, entities.CharacterVoiceActor{ - Language: va.Language, + for _, voiceActor := range jikanCharacter.VoiceActors { + character.VoiceActors = append(character.VoiceActors, entities.CharacterVoiceActor{ + Language: voiceActor.Language, Person: &entities.Person{ - Image: va.Person.Images.JPG.ImageURL, + Image: voiceActor.Person.Images.JPG.ImageURL, }, }) } - anime.Characters = append(anime.Characters, char) + anime.Characters = append(anime.Characters, character) } } } @@ -360,8 +422,8 @@ func applyAnilistData(anime *entities.Anime, anilistData *types.AnilistAnimeResp anime.Color = media.CoverImage.Color } - if anime.Covers == nil && (media.CoverImage.Medium != "" || media.CoverImage.Large != "" || media.CoverImage.ExtraLarge != "") { - anime.Covers = &entities.Images{ + if anime.Covers.Original == "" && (media.CoverImage.Medium != "" || media.CoverImage.Large != "" || media.CoverImage.ExtraLarge != "") { + anime.Covers = entities.AnimeImages{ Small: media.CoverImage.Medium, Large: media.CoverImage.Large, Original: media.CoverImage.ExtraLarge, @@ -369,16 +431,14 @@ func applyAnilistData(anime *entities.Anime, anilistData *types.AnilistAnimeResp } if media.NextAiringEpisode.AiringAt > 0 { - anime.NextAiring = &entities.NextEpisode{ - Episode: media.NextAiringEpisode.Episode, - AiringAt: media.NextAiringEpisode.AiringAt, - } + anime.NextAiringAt = media.NextAiringEpisode.AiringAt + anime.NextAiringEpisode = media.NextAiringEpisode.Episode } - for _, ep := range media.AiringSchedule.Nodes { + for _, scheduleNode := range media.AiringSchedule.Nodes { anime.Schedule = append(anime.Schedule, entities.EpisodeSchedule{ - Episode: ep.Episode, - AiringAt: ep.AiringAt, + Episode: scheduleNode.Episode, + AiringAt: scheduleNode.AiringAt, }) } } @@ -389,20 +449,20 @@ func applyMALsyncData(anime *entities.Anime, malSyncData *types.MalsyncAnimeResp } logos := extractLogosFromMALSync(malSyncData) - if logos != nil { + if logos.Small != "" { anime.Logos = logos } } -func extractLogosFromMALSync(malSyncData *types.MalsyncAnimeResponse) *entities.Logos { +func extractLogosFromMALSync(malSyncData *types.MalsyncAnimeResponse) entities.AnimeLogos { if malSyncData == nil { - return nil + return entities.AnimeLogos{} } crunchyrollSites, exists := malSyncData.Sites["Crunchyroll"] if !exists || len(crunchyrollSites) == 0 { logger.Debugf("AnimeService", "No Crunchyroll data found in MALSync response") - return nil + return entities.AnimeLogos{} } crURL := "" @@ -413,12 +473,12 @@ func extractLogosFromMALSync(malSyncData *types.MalsyncAnimeResponse) *entities. if crURL == "" { logger.Debugf("AnimeService", "No valid Crunchyroll URL found") - return nil + return entities.AnimeLogos{} } seriesID := extractCrunchyrollSeriesID(crURL) if seriesID == "" { - return nil + return entities.AnimeLogos{} } logoSizes := map[string]int{ @@ -429,15 +489,13 @@ func extractLogosFromMALSync(malSyncData *types.MalsyncAnimeResponse) *entities. "Original": 1200, } - logos := &entities.Logos{ + return entities.AnimeLogos{ 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), 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), 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), 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), 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), } - - return logos } func extractCrunchyrollSeriesID(crURL string) string { @@ -478,10 +536,6 @@ func applyAniskipData(episode *entities.Episode, skipData []types.AniskipResult) } func applyStreamingData(anime *entities.Anime) { - if anime.Title == nil { - return - } - searchTitle := anime.Title.Romaji if searchTitle == "" { searchTitle = anime.Title.English @@ -507,26 +561,26 @@ func applyStreamingData(anime *entities.Anime) { anime.DubbedCount = dubCount if len(anime.Episodes) > 0 { - epNums := make([]int, len(anime.Episodes)) - for i, ep := range anime.Episodes { - epNums[i] = ep.EpisodeNumber + episodeNumbers := make([]int, len(anime.Episodes)) + for i, episode := range anime.Episodes { + episodeNumbers[i] = episode.EpisodeNumber } - sourcesMap, err := streaming.FetchAllEpisodeSources(searchTitle, epNums) + sourcesMap, err := streaming.FetchAllEpisodeSources(searchTitle, episodeNumbers) if err == nil { for i := range anime.Episodes { - ep := &anime.Episodes[i] - if s, ok := sourcesMap[ep.EpisodeNumber]; ok { - sub := make([]entities.StreamingSource, len(s.Sub)) - for j, src := range s.Sub { - sub[j] = entities.StreamingSource{URL: src.URL, Server: src.Server, Type: src.Type} + episode := &anime.Episodes[i] + if sources, ok := sourcesMap[episode.EpisodeNumber]; ok { + subSources := make([]entities.StreamingSource, len(sources.Sub)) + for j, source := range sources.Sub { + subSources[j] = entities.StreamingSource{URL: source.URL, Server: source.Server, Type: source.Type} } - dub := make([]entities.StreamingSource, len(s.Dub)) - for j, src := range s.Dub { - dub[j] = entities.StreamingSource{URL: src.URL, Server: src.Server, Type: src.Type} + dubSources := make([]entities.StreamingSource, len(sources.Dub)) + for j, source := range sources.Dub { + dubSources[j] = entities.StreamingSource{URL: source.URL, Server: source.Server, Type: source.Type} } - ep.StreamInfo = &entities.StreamInfo{ - SubSources: sub, - DubSources: dub, + episode.StreamInfo = &entities.StreamInfo{ + SubSources: subSources, + DubSources: dubSources, } } } @@ -548,10 +602,10 @@ func applySeasonData(anime *entities.Anime, mapping *entities.Mapping) { tvdbMappings, err := repositories.GetRelatedAnimeByTVDB(mapping.TVDB, mapping.MAL) if err == nil && len(tvdbMappings) > 0 { logger.Infof("AnimeService", "Found %d related anime via TVDB", len(tvdbMappings)) - for _, m := range tvdbMappings { - if !malIDSet[m.MAL] { - malIDSet[m.MAL] = true - relatedMappings = append(relatedMappings, m) + for _, relatedMapping := range tvdbMappings { + if !malIDSet[relatedMapping.MAL] { + malIDSet[relatedMapping.MAL] = true + relatedMappings = append(relatedMappings, relatedMapping) } } } @@ -561,10 +615,10 @@ func applySeasonData(anime *entities.Anime, mapping *entities.Mapping) { tmdbMappings, err := repositories.GetRelatedAnimeByTMDB(mapping.TMDB, mapping.MAL) if err == nil && len(tmdbMappings) > 0 { logger.Infof("AnimeService", "Found %d related anime via TMDB", len(tmdbMappings)) - for _, m := range tmdbMappings { - if !malIDSet[m.MAL] { - malIDSet[m.MAL] = true - relatedMappings = append(relatedMappings, m) + for _, relatedMapping := range tmdbMappings { + if !malIDSet[relatedMapping.MAL] { + malIDSet[relatedMapping.MAL] = true + relatedMappings = append(relatedMappings, relatedMapping) } } } @@ -600,63 +654,14 @@ func applySeasonData(anime *entities.Anime, mapping *entities.Mapping) { }) season := entities.Season{ - MALID: relatedMapping.MAL, - Synopsis: seasonAnime.Data.Synopsis, - Type: seasonAnime.Data.Type, - Source: seasonAnime.Data.Source, - Airing: seasonAnime.Data.Airing, - Status: seasonAnime.Data.Status, - Duration: seasonAnime.Data.Duration, - Season: seasonAnime.Data.Season, - Year: seasonAnime.Data.Year, - } - - if seasonAnime.Data.Title != "" || seasonAnime.Data.TitleEnglish != "" || seasonAnime.Data.TitleJapanese != "" { - season.Title = &entities.Title{ - Romaji: seasonAnime.Data.Title, - English: seasonAnime.Data.TitleEnglish, - Japanese: seasonAnime.Data.TitleJapanese, - Synonyms: seasonAnime.Data.TitleSynonyms, - } - } - - if seasonAnime.Data.Images.JPG.ImageURL != "" { - season.Images = &entities.Images{ - Small: seasonAnime.Data.Images.JPG.SmallImageURL, - Large: seasonAnime.Data.Images.JPG.LargeImageURL, - Original: seasonAnime.Data.Images.JPG.ImageURL, - } - } - - season.Scores = &entities.Scores{ - Score: seasonAnime.Data.Score, - ScoredBy: seasonAnime.Data.ScoredBy, - Rank: seasonAnime.Data.Rank, - Popularity: seasonAnime.Data.Popularity, - Members: seasonAnime.Data.Members, - Favorites: seasonAnime.Data.Favorites, - } - - if seasonAnime.Data.Aired.From != "" || seasonAnime.Data.Aired.To != "" { - season.AiringStatus = &entities.AiringStatus{ - String: seasonAnime.Data.Aired.String, - } - if seasonAnime.Data.Aired.Prop.From.Year > 0 { - season.AiringStatus.From = &entities.Date{ - Day: seasonAnime.Data.Aired.Prop.From.Day, - Month: seasonAnime.Data.Aired.Prop.From.Month, - Year: seasonAnime.Data.Aired.Prop.From.Year, - String: seasonAnime.Data.Aired.From, - } - } - if seasonAnime.Data.Aired.Prop.To.Year > 0 { - season.AiringStatus.To = &entities.Date{ - Day: seasonAnime.Data.Aired.Prop.To.Day, - Month: seasonAnime.Data.Aired.Prop.To.Month, - Year: seasonAnime.Data.Aired.Prop.To.Year, - String: seasonAnime.Data.Aired.To, - } - } + MALID: relatedMapping.MAL, + TitleEnglish: seasonAnime.Data.TitleEnglish, + TitleRomaji: seasonAnime.Data.Title, + ImageOriginal: seasonAnime.Data.Images.JPG.ImageURL, + Year: seasonAnime.Data.Year, + SeasonName: seasonAnime.Data.Season, + Type: seasonAnime.Data.Type, + Status: seasonAnime.Data.Status, } anime.Seasons = append(anime.Seasons, season) @@ -665,9 +670,9 @@ func applySeasonData(anime *entities.Anime, mapping *entities.Mapping) { sortSeasonsByChronology(allSeasons) seasonNumberMap := make(map[int]int) - for i, s := range allSeasons { - seasonNumberMap[s.malID] = i + 1 - if s.isCurrent { + for i, seasonEntry := range allSeasons { + seasonNumberMap[seasonEntry.malID] = i + 1 + if seasonEntry.isCurrent { anime.SeasonNumber = i + 1 } } @@ -697,11 +702,11 @@ func getSeasonOrder(season string) int { func sortSeasonsByChronology(seasons []seasonInfo) { for i := 0; i < len(seasons)-1; i++ { for j := 0; j < len(seasons)-i-1; j++ { - s1, s2 := seasons[j], seasons[j+1] + current, next := seasons[j], seasons[j+1] - if s1.year > s2.year { + if current.year > next.year { seasons[j], seasons[j+1] = seasons[j+1], seasons[j] - } else if s1.year == s2.year && s1.seasonOrder > s2.seasonOrder { + } else if current.year == next.year && current.seasonOrder > next.seasonOrder { seasons[j], seasons[j+1] = seasons[j+1], seasons[j] } } @@ -721,6 +726,18 @@ func saveAnime(anime *entities.Anime, skipTimeMap map[string][]entities.EpisodeS } } + for i := range anime.Themes { + if err := repositories.CreateOrUpdateGenre(&anime.Themes[i]); err != nil { + logger.Warnf("AnimeService", "Failed to save theme: %v", err) + } + } + + for i := range anime.Demographics { + if err := repositories.CreateOrUpdateGenre(&anime.Demographics[i]); err != nil { + logger.Warnf("AnimeService", "Failed to save demographic: %v", err) + } + } + for i := range anime.Producers { if err := repositories.CreateOrUpdateProducer(&anime.Producers[i]); err != nil { logger.Warnf("AnimeService", "Failed to save producer: %v", err) @@ -748,10 +765,10 @@ func saveAnime(anime *entities.Anime, skipTimeMap map[string][]entities.EpisodeS logger.Warnf("AnimeService", "Failed to save episodes: %v", err) } for i := range anime.Episodes { - ep := &anime.Episodes[i] - if ep.StreamInfo != nil && ep.EpisodeID != "" { - if err := repositories.SaveEpisodeStreamInfo(anime.ID, ep.EpisodeID, ep.StreamInfo); err != nil { - logger.Warnf("AnimeService", "Failed to save stream info for episode %s: %v", ep.EpisodeID, err) + episode := &anime.Episodes[i] + if episode.StreamInfo != nil && episode.EpisodeID != "" { + if err := repositories.SaveEpisodeStreamInfo(anime.ID, episode.EpisodeID, episode.StreamInfo); err != nil { + logger.Warnf("AnimeService", "Failed to save stream info for episode %s: %v", episode.EpisodeID, err) } } } diff --git a/tasks/aniupdate.task.go b/tasks/aniupdate.task.go index 9952d23..3218573 100644 --- a/tasks/aniupdate.task.go +++ b/tasks/aniupdate.task.go @@ -70,21 +70,17 @@ func AnimeUpdate() error { needsUpdate := false reason := "" - title := "" - if series.Title != nil { - if series.Title.Romaji != "" { - title = series.Title.Romaji - } else if series.Title.English != "" { - title = series.Title.English - } + title := series.Title.Romaji + if title == "" { + title = series.Title.English } logger.Debugf("AnimeUpdate", "Checking anime: %s (ID: %d)", title, series.MALID) - if series.NextAiring == nil || series.NextAiring.AiringAt == 0 { + if series.NextAiringAt == 0 { needsUpdate = true reason = "missing next episode data" - } else if int64(series.NextAiring.AiringAt) <= currentTime { + } else if int64(series.NextAiringAt) <= currentTime { needsUpdate = true reason = "next episode already aired" } @@ -97,7 +93,7 @@ func AnimeUpdate() error { if !needsUpdate { logger.Debugf("AnimeUpdate", "Skipping update for %s (ID: %d) - no update needed. Next airing at: %d", - title, series.MALID, series.NextAiring.AiringAt) + title, series.MALID, series.NextAiringAt) continue } @@ -121,13 +117,9 @@ func AnimeUpdate() error { } func updateAnime(series entities.Anime, reason string) { - title := "" - if series.Title != nil { - if series.Title.English != "" { - title = series.Title.English - } else if series.Title.Romaji != "" { - title = series.Title.Romaji - } + title := series.Title.English + if title == "" { + title = series.Title.Romaji } logger.Infof("AnimeUpdate", "Updating anime: %s (MAL ID: %d) - %s", title, series.MALID, reason) @@ -170,16 +162,16 @@ func shouldSaveUpdate(oldAnime *entities.Anime, newAnime *entities.Anime) bool { return true } - oldHasNext := oldAnime.NextAiring != nil && oldAnime.NextAiring.AiringAt > 0 - newHasNext := newAnime.NextAiring != nil && newAnime.NextAiring.AiringAt > 0 + oldHasNext := oldAnime.NextAiringAt > 0 + newHasNext := newAnime.NextAiringAt > 0 if oldHasNext != newHasNext { return true } if oldHasNext && newHasNext { - if oldAnime.NextAiring.AiringAt != newAnime.NextAiring.AiringAt || - oldAnime.NextAiring.Episode != newAnime.NextAiring.Episode { + if oldAnime.NextAiringAt != newAnime.NextAiringAt || + oldAnime.NextAiringEpisode != newAnime.NextAiringEpisode { return true } } diff --git a/tasks/manager.go b/tasks/manager.go index 30522a5..fb57e25 100644 --- a/tasks/manager.go +++ b/tasks/manager.go @@ -104,7 +104,6 @@ func (tm *TaskManager) StartTask(taskName string) { tm.logTaskExecution(taskName, "error", err.Error()) logger.Errorf("TaskManager", "Task %s execution failed: %v", taskName, err) } else { - task.LastRun = time.Now() tm.logTaskExecution(taskName, "success", "Task executed successfully") repositories.SetTaskStatus(&entities.TaskStatus{ TaskName: taskName, @@ -134,7 +133,6 @@ func (tm *TaskManager) StartTask(taskName string) { tm.logTaskExecution(taskName, "error", err.Error()) logger.Errorf("TaskManager", "Task %s execution failed: %v", taskName, err) } else { - task.LastRun = time.Now() tm.logTaskExecution(taskName, "success", "Task executed successfully") repositories.SetTaskStatus(&entities.TaskStatus{ TaskName: taskName, @@ -168,7 +166,6 @@ func (tm *TaskManager) StartTask(taskName string) { tm.logTaskExecution(taskName, "error", err.Error()) logger.Errorf("TaskManager", "Task %s execution failed: %v", taskName, err) } else { - task.LastRun = time.Now() tm.logTaskExecution(taskName, "success", "Task executed successfully") repositories.SetTaskStatus(&entities.TaskStatus{ TaskName: taskName, @@ -246,46 +243,42 @@ func (tm *TaskManager) checkDependencies(task types.Task) bool { func (tm *TaskManager) triggerDependentTasks(completedTaskName string) { tm.Mutex.Lock() - defer tm.Mutex.Unlock() - - for taskName, task := range tm.Tasks { - hasDependency := false - for _, dep := range task.Dependencies { + type dependentTask struct { + taskName string + taskDefinition types.Task + } + var dependentTasks []dependentTask + for registeredName, registeredTask := range tm.Tasks { + for _, dep := range registeredTask.Dependencies { if dep == completedTaskName { - hasDependency = true + dependentTasks = append(dependentTasks, dependentTask{taskName: registeredName, taskDefinition: registeredTask}) break } } + } + tm.Mutex.Unlock() - if !hasDependency { + for _, dependent := range dependentTasks { + if !tm.checkDependencies(dependent.taskDefinition) { continue } - tm.Mutex.Unlock() - allDependenciesMet := tm.checkDependencies(task) - tm.Mutex.Lock() - - if allDependenciesMet { - logger.Infof("TaskManager", "All dependencies met for %s, triggering execution", taskName) - go func(name string, t types.Task) { - if err := t.Execute(); err != nil { - tm.logTaskExecution(name, "error", err.Error()) - logger.Errorf("TaskManager", "Task %s execution failed: %v", name, err) - } else { - tm.Mutex.Lock() - t.LastRun = time.Now() - tm.Mutex.Unlock() - tm.logTaskExecution(name, "success", "Task executed successfully") - repositories.SetTaskStatus(&entities.TaskStatus{ - TaskName: name, - IsCompleted: true, - LastRunAt: time.Now(), - }) - logger.Successf("TaskManager", "Task %s executed successfully", name) - tm.triggerDependentTasks(name) - } - }(taskName, task) - } + logger.Infof("TaskManager", "All dependencies met for %s, triggering execution", dependent.taskName) + go func(name string, task types.Task) { + if err := task.Execute(); err != nil { + tm.logTaskExecution(name, "error", err.Error()) + logger.Errorf("TaskManager", "Task %s execution failed: %v", name, err) + } else { + tm.logTaskExecution(name, "success", "Task executed successfully") + repositories.SetTaskStatus(&entities.TaskStatus{ + TaskName: name, + IsCompleted: true, + LastRunAt: time.Now(), + }) + logger.Successf("TaskManager", "Task %s executed successfully", name) + tm.triggerDependentTasks(name) + } + }(dependent.taskName, dependent.taskDefinition) } } @@ -320,13 +313,16 @@ func (tm *TaskManager) GetTaskStatus(taskName string) *types.TaskStatus { } func (tm *TaskManager) GetAllTaskStatuses() map[string]*types.TaskStatus { - statuses := make(map[string]*types.TaskStatus) tm.Mutex.Lock() + taskNames := make([]string, 0, len(tm.Tasks)) for name := range tm.Tasks { - tm.Mutex.Unlock() - statuses[name] = tm.GetTaskStatus(name) - tm.Mutex.Lock() + taskNames = append(taskNames, name) } tm.Mutex.Unlock() + + statuses := make(map[string]*types.TaskStatus, len(taskNames)) + for _, name := range taskNames { + statuses[name] = tm.GetTaskStatus(name) + } return statuses } diff --git a/types/jikan.go b/types/jikan.go index 45434d3..b3279b8 100644 --- a/types/jikan.go +++ b/types/jikan.go @@ -112,33 +112,29 @@ type JikanSingleAnime struct { Season string `json:"season"` Year int `json:"year"` Images JikanGenericImageEntity `json:"images"` + Trailer JikanAnimeTrailer `json:"trailer"` + Approved bool `json:"approved"` + Titles []JikanGenericTitleEntity `json:"titles"` + Aired JikanAiringSchedule `json:"aired"` + Duration string `json:"duration"` + Rating string `json:"rating"` + Background string `json:"background"` + Broadcast JikanBroadcastSchedule `json:"broadcast"` Genres []JikanGenericRelatedEntity `json:"genres"` ExplicitGenres []JikanGenericRelatedEntity `json:"explicit_genres"` + Themes []JikanGenericRelatedEntity `json:"themes"` + Demographics []JikanGenericRelatedEntity `json:"demographics"` Producers []JikanGenericRelatedEntity `json:"producers"` Licensors []JikanGenericRelatedEntity `json:"licensors"` Studios []JikanGenericRelatedEntity `json:"studios"` -} - -type JikanFullSingleAnime struct { - JikanSingleAnime - Trailer JikanAnimeTrailer `json:"trailer"` - Approved bool `json:"approved"` - Titles []JikanGenericTitleEntity `json:"titles"` - Aired JikanAiringSchedule `json:"aired"` - Duration string `json:"duration"` - Rating string `json:"rating"` - Background string `json:"background"` - Broadcast JikanBroadcastSchedule `json:"broadcast"` - Themes []JikanGenericRelatedEntity `json:"themes"` - Demographics []JikanGenericRelatedEntity `json:"demographics"` - Relations []JikanGenericRelation `json:"relations"` - Theme JikanAnimeTheme `json:"theme"` - External []JikanGenericURL `json:"external"` - Streaming []JikanGenericURL `json:"streaming"` + Relations []JikanGenericRelation `json:"relations"` + Theme JikanAnimeTheme `json:"theme"` + External []JikanGenericURL `json:"external"` + Streaming []JikanGenericURL `json:"streaming"` } type JikanAnimeResponse struct { - Data JikanFullSingleAnime `json:"data"` + Data JikanSingleAnime `json:"data"` } type JikanAnimeSingleEpisode struct { diff --git a/types/tasks.go b/types/tasks.go index f86a497..720987a 100644 --- a/types/tasks.go +++ b/types/tasks.go @@ -7,7 +7,6 @@ type Task struct { Interval time.Duration Execute func() error OnResume func() - LastRun time.Time Dependencies []string } diff --git a/utils/api/aniskip/aniskip.go b/utils/api/aniskip/aniskip.go index d7cc343..96220d1 100644 --- a/utils/api/aniskip/aniskip.go +++ b/utils/api/aniskip/aniskip.go @@ -39,6 +39,10 @@ var ( } ) +func StopRateLimiters() { + rateLimiter.Stop() +} + func (c *client) getBackOffDuration(attempt int) time.Duration { return time.Duration(float64(c.backoff) * math.Pow(2, float64(attempt-1))) } diff --git a/utils/api/jikan/jikan.go b/utils/api/jikan/jikan.go index c5bb2c8..991186e 100644 --- a/utils/api/jikan/jikan.go +++ b/utils/api/jikan/jikan.go @@ -39,6 +39,10 @@ var ( } ) +func StopRateLimiters() { + rateLimiter.Stop() +} + func (c *client) getBackOffDuration(attempt int) time.Duration { return time.Duration(float64(c.backoff) * math.Pow(2, float64(attempt-1))) } diff --git a/utils/api/tmdb/tmdb.go b/utils/api/tmdb/tmdb.go index 99f6670..f4c233a 100644 --- a/utils/api/tmdb/tmdb.go +++ b/utils/api/tmdb/tmdb.go @@ -312,12 +312,8 @@ func AttachEpisodeDescriptions(anime *entities.Anime) error { return nil } - title := "" - alternativeTitle := "" - if anime.Title != nil { - title = anime.Title.Romaji - alternativeTitle = anime.Title.English - } + title := anime.Title.Romaji + alternativeTitle := anime.Title.English tmdbID := 0 malID := anime.MALID @@ -443,12 +439,10 @@ func AttachEpisodeDescriptions(anime *entities.Anime) error { episode.EpisodeNumber = tmdbEpisodes[i].EpisodeNumber titleForID := "" - if episode.Title != nil { - if episode.Title.English != "" { - titleForID = episode.Title.English - } else if episode.Title.Romaji != "" { - titleForID = episode.Title.Romaji - } + if episode.Title.English != "" { + titleForID = episode.Title.English + } else if episode.Title.Romaji != "" { + titleForID = episode.Title.Romaji } if titleForID == "" && tmdbEpisodes[i].Name != "" { titleForID = tmdbEpisodes[i].Name @@ -565,14 +559,9 @@ func EnrichEpisodeFromMovie(anime *entities.Anime) error { episode := &anime.Episodes[0] - title := "" - alternativeTitle := "" - japaneseTitle := "" - if anime.Title != nil { - title = anime.Title.Romaji - alternativeTitle = anime.Title.English - japaneseTitle = anime.Title.Japanese - } + title := anime.Title.Romaji + alternativeTitle := anime.Title.English + japaneseTitle := anime.Title.Japanese tmdbID := 0 malID := anime.MALID @@ -580,10 +569,7 @@ func EnrichEpisodeFromMovie(anime *entities.Anime) error { tmdbID = anime.Mapping.TMDB } - animeScore := 0.0 - if anime.Scores != nil { - animeScore = anime.Scores.Score - } + animeScore := anime.Scores.Score logger.Debugf("TMDB", "Fetching movie episode data for: %s", title) @@ -622,12 +608,11 @@ func EnrichEpisodeFromMovie(anime *entities.Anime) error { description = noDescription } - if episode.Title == nil { - episode.Title = &entities.Title{} + episode.Title = entities.EpisodeTitle{ + English: movieDetails.Title, + Japanese: japaneseTitle, + Romaji: title, } - episode.Title.English = movieDetails.Title - episode.Title.Japanese = japaneseTitle - episode.Title.Romaji = title movieScore := float64(int((animeScore/2.0)*100)) / 100 diff --git a/utils/api/tvdb/tvdb.go b/utils/api/tvdb/tvdb.go index 5d6f78d..ee8f0a9 100644 --- a/utils/api/tvdb/tvdb.go +++ b/utils/api/tvdb/tvdb.go @@ -152,11 +152,12 @@ func EnrichEpisodesFromTVDB(anime *entities.Anime, tvdbEpisodes []types.TVDBEpis episode := &anime.Episodes[i] - if episode.Title == nil { - episode.Title = &entities.Title{} - } if ep.Name != "" { - episode.Title.English = ep.Name + episode.Title = entities.EpisodeTitle{ + English: ep.Name, + Japanese: episode.Title.Japanese, + Romaji: episode.Title.Romaji, + } } if ep.Image != "" { @@ -181,7 +182,7 @@ func EnrichEpisodesFromTVDB(anime *entities.Anime, tvdbEpisodes []types.TVDBEpis episode.EpisodeLength = float64(ep.Runtime) titleForID := ep.Name - if titleForID == "" && episode.Title != nil { + if titleForID == "" { if episode.Title.English != "" { titleForID = episode.Title.English } else if episode.Title.Romaji != "" { diff --git a/utils/ratelimit/limiter.go b/utils/ratelimit/limiter.go index c99a935..fdf6778 100644 --- a/utils/ratelimit/limiter.go +++ b/utils/ratelimit/limiter.go @@ -54,3 +54,9 @@ func (m *MultiLimiter) Wait() { limiter.Wait() } } + +func (m *MultiLimiter) Stop() { + for _, limiter := range m.limiters { + limiter.Stop() + } +} |
