aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-02-25 10:42:51 +0530
committerBobby <[email protected]>2026-02-25 10:42:51 +0530
commitc18acfafee26d7d2508848135aa94a524f8dde06 (patch)
treee0b4d5e07f872d9252af6d73fb8b108e15a49f91
parent2df69fab61b580b6b329db214ee0025a9d84958d (diff)
downloadmetachan-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.go9
-rw-r--r--database/migrate.go9
-rw-r--r--entities/anime.go138
-rw-r--r--entities/episode.go40
-rw-r--r--entities/meta.go59
-rw-r--r--entities/persona.go12
-rw-r--r--entities/seasons.go31
-rw-r--r--go.mod2
-rw-r--r--go.sum2
-rw-r--r--metachan/main.go35
-rw-r--r--repositories/anime.go30
-rw-r--r--repositories/persona.go264
-rw-r--r--services/anime.go477
-rw-r--r--tasks/aniupdate.task.go34
-rw-r--r--tasks/manager.go74
-rw-r--r--types/jikan.go34
-rw-r--r--types/tasks.go1
-rw-r--r--utils/api/aniskip/aniskip.go4
-rw-r--r--utils/api/jikan/jikan.go4
-rw-r--r--utils/api/tmdb/tmdb.go43
-rw-r--r--utils/api/tvdb/tvdb.go11
-rw-r--r--utils/ratelimit/limiter.go6
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"`
}
diff --git a/go.mod b/go.mod
index e8f28c7..23654d0 100644
--- a/go.mod
+++ b/go.mod
@@ -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
)
diff --git a/go.sum b/go.sum
index 1e91e58..c37ddd1 100644
--- a/go.sum
+++ b/go.sum
@@ -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()
+ }
+}