aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-02-24 17:36:55 +0530
committerBobby <[email protected]>2026-02-24 17:36:55 +0530
commit2df69fab61b580b6b329db214ee0025a9d84958d (patch)
treebf7d69e1cfc5f6dc3e387f99325e6842e7dc60c1
parentd3507ae5b9d88a250b444c0e996fa07f84f6e3c5 (diff)
downloadmetachan-2df69fab61b580b6b329db214ee0025a9d84958d.tar.xz
metachan-2df69fab61b580b6b329db214ee0025a9d84958d.zip
feat: Enhance person handling and synchronization logic
- Introduced new Person entity with detailed attributes - Updated repositories and controllers to support person data retrieval - Implemented PersonSync task for background enrichment of person data - Refactored existing character and voice actor logic to utilize Person entity - Added Jikan API integration for fetching person details
-rw-r--r--controllers/persona.go39
-rw-r--r--database/migrate.go22
-rw-r--r--entities/persona.go66
-rw-r--r--repositories/persona.go195
-rw-r--r--repositories/types.go5
-rw-r--r--router/router.go8
-rw-r--r--services/anime.go5
-rw-r--r--tasks/charactersync.task.go7
-rw-r--r--tasks/personasync.task.go134
-rw-r--r--tasks/tasks.go17
-rw-r--r--types/jikan.go37
-rw-r--r--utils/api/jikan/jikan.go19
12 files changed, 492 insertions, 62 deletions
diff --git a/controllers/persona.go b/controllers/persona.go
index d90157d..4beef53 100644
--- a/controllers/persona.go
+++ b/controllers/persona.go
@@ -29,11 +29,26 @@ func GetAnimeCharacters(c *fiber.Ctx) error {
}
func GetAnimeCharacter(c *fiber.Ctx) error {
- id := meta.Request(c).MustHave().Param("id")
characterID, ok := meta.Request(c).Param("characterId")
if !ok {
return BadRequest(c, errors.New("characterId is required"))
}
+
+ malID, err := strconv.Atoi(characterID)
+ if err != nil {
+ return BadRequest(c, errors.New("characterId must be a numeric MAL ID"))
+ }
+
+ character, err := repositories.GetCharacterByMALID(malID)
+ if err != nil {
+ return NotFound(c, err)
+ }
+
+ return c.JSON(character)
+}
+
+func GetAnimePeople(c *fiber.Ctx) error {
+ id := meta.Request(c).MustHave().Param("id")
provider := meta.Request(c).Default("mal").Query("provider")
switch provider {
@@ -42,15 +57,29 @@ func GetAnimeCharacter(c *fiber.Ctx) error {
return BadRequest(c, errors.New("invalid provider"))
}
- malID, err := strconv.Atoi(characterID)
+ people, err := repositories.GetAnimePeople(enums.MappingType(provider), id)
if err != nil {
- return BadRequest(c, errors.New("characterId must be a numeric MAL ID"))
+ return NotFound(c, err)
}
- character, err := repositories.GetAnimeCharacter(enums.MappingType(provider), id, malID)
+ return c.JSON(people)
+}
+
+func GetPerson(c *fiber.Ctx) error {
+ personID, ok := meta.Request(c).Param("personId")
+ if !ok {
+ return BadRequest(c, errors.New("personId is required"))
+ }
+
+ malID, err := strconv.Atoi(personID)
+ if err != nil {
+ return BadRequest(c, errors.New("personId must be a numeric MAL ID"))
+ }
+
+ person, err := repositories.GetPerson(malID)
if err != nil {
return NotFound(c, err)
}
- return c.JSON(character)
+ return c.JSON(person)
}
diff --git a/database/migrate.go b/database/migrate.go
index 830467a..3fe542e 100644
--- a/database/migrate.go
+++ b/database/migrate.go
@@ -7,14 +7,9 @@ import (
func migrate() {
err := DB.AutoMigrate(
- // Task entities
&entities.TaskLog{},
&entities.TaskStatus{},
-
- // Mapping entity
&entities.Mapping{},
-
- // Meta entities (shared/reusable)
&entities.Title{},
&entities.Scores{},
&entities.Date{},
@@ -25,32 +20,23 @@ func migrate() {
&entities.ExternalURL{},
&entities.SimpleTitle{},
&entities.SimpleImage{},
-
- // Genre entity
&entities.Genre{},
-
- // Producer entity
&entities.Producer{},
-
- // Anime entity
&entities.Anime{},
-
- // Episode entities
&entities.Episode{},
&entities.EpisodeSkipTime{},
&entities.StreamingSource{},
&entities.EpisodeSchedule{},
&entities.NextEpisode{},
-
- // Season entity
&entities.Season{},
-
- // Character/Persona entities
&entities.Character{},
- &entities.VoiceActor{},
+ &entities.Person{},
&entities.AnimeCharacter{},
&entities.CharacterVoiceActor{},
&entities.CharacterAnimeAppearance{},
+ &entities.PersonVoiceRole{},
+ &entities.PersonAnimeCredit{},
+ &entities.PersonMangaCredit{},
)
if err != nil {
logger.Fatalf("Database", "Error during database migration: %v", err)
diff --git a/entities/persona.go b/entities/persona.go
index f722eae..9390b96 100644
--- a/entities/persona.go
+++ b/entities/persona.go
@@ -27,12 +27,60 @@ type CharacterAnimeAppearance struct {
Role string `json:"role,omitempty"`
}
-type VoiceActor struct {
+type Person struct {
BaseModel
- MALID int `gorm:"uniqueIndex" json:"mal_id,omitempty"`
- URL string `json:"url,omitempty"`
- Image string `json:"image_url,omitempty"`
- Name string `json:"name,omitempty"`
+ MALID int `gorm:"uniqueIndex" json:"mal_id,omitempty"`
+ URL string `json:"url,omitempty"`
+ WebsiteURL string `json:"website_url,omitempty"`
+ Image string `json:"image_url,omitempty"`
+ Name string `json:"name,omitempty"`
+ GivenName string `json:"given_name,omitempty"`
+ FamilyName string `json:"family_name,omitempty"`
+ AlternateNames []string `gorm:"serializer:json" json:"alternate_names,omitempty"`
+ Birthday *time.Time `json:"birthday,omitempty"`
+ Favorites int `json:"favorites,omitempty"`
+ About string `gorm:"type:text" json:"about,omitempty"`
+ EnrichedAt *time.Time `json:"-"`
+ Characters []PersonCharacterEntry `gorm:"-" json:"characters,omitempty"`
+ VoiceRoles []PersonVoiceRole `gorm:"foreignKey:PersonID" json:"voices,omitempty"`
+ AnimeCredits []PersonAnimeCredit `gorm:"foreignKey:PersonID" json:"anime,omitempty"`
+ MangaCredits []PersonMangaCredit `gorm:"foreignKey:PersonID" json:"manga,omitempty"`
+}
+
+type PersonCharacterEntry struct {
+ Character *Character `json:"character,omitempty"`
+ Language string `json:"language,omitempty"`
+}
+
+type PersonVoiceRole struct {
+ PersonID uint `gorm:"primaryKey" json:"-"`
+ AnimeMALID int `gorm:"primaryKey" json:"anime_mal_id,omitempty"`
+ CharacterMALID int `gorm:"primaryKey" json:"character_mal_id,omitempty"`
+ Role string `json:"role,omitempty"`
+ AnimeTitle string `json:"anime_title,omitempty"`
+ AnimeURL string `json:"anime_url,omitempty"`
+ AnimeImageURL string `json:"anime_image_url,omitempty"`
+ CharacterName string `json:"character_name,omitempty"`
+ CharacterURL string `json:"character_url,omitempty"`
+ CharacterImageURL string `json:"character_image_url,omitempty"`
+}
+
+type PersonAnimeCredit struct {
+ PersonID uint `gorm:"primaryKey" json:"-"`
+ AnimeMALID int `gorm:"primaryKey" json:"mal_id,omitempty"`
+ Position string `json:"position,omitempty"`
+ AnimeTitle string `json:"title,omitempty"`
+ AnimeURL string `json:"url,omitempty"`
+ AnimeImageURL string `json:"image_url,omitempty"`
+}
+
+type PersonMangaCredit struct {
+ PersonID uint `gorm:"primaryKey" json:"-"`
+ MangaMALID int `gorm:"primaryKey" json:"mal_id,omitempty"`
+ Position string `json:"position,omitempty"`
+ MangaTitle string `json:"title,omitempty"`
+ MangaURL string `json:"url,omitempty"`
+ MangaImageURL string `json:"image_url,omitempty"`
}
type AnimeCharacter struct {
@@ -42,8 +90,8 @@ type AnimeCharacter struct {
}
type CharacterVoiceActor struct {
- CharacterID uint `gorm:"primaryKey" json:"-"`
- VoiceActorID uint `gorm:"primaryKey" json:"-"`
- Language string `json:"language,omitempty"`
- VoiceActor *VoiceActor `gorm:"foreignKey:VoiceActorID;references:ID" json:"voice_actor,omitempty"`
+ CharacterID uint `gorm:"primaryKey" json:"-"`
+ PersonID uint `gorm:"primaryKey" json:"-"`
+ Language string `json:"language,omitempty"`
+ Person *Person `gorm:"foreignKey:PersonID;references:ID" json:"person,omitempty"`
}
diff --git a/repositories/persona.go b/repositories/persona.go
index 1c70230..1cb808c 100644
--- a/repositories/persona.go
+++ b/repositories/persona.go
@@ -38,7 +38,7 @@ func UpdateCharacterDetails(malID int, name, nameKanji, url, imageURL, about str
DB.Where("character_id = ?", char.ID).Delete(&entities.CharacterVoiceActor{})
for _, cva := range voiceActors {
- va := cva.VoiceActor
+ va := cva.Person
if va == nil {
continue
}
@@ -52,12 +52,12 @@ func UpdateCharacterDetails(malID int, name, nameKanji, url, imageURL, about str
}
DB.Clauses(clause.OnConflict{
- Columns: []clause.Column{{Name: "character_id"}, {Name: "voice_actor_id"}},
+ Columns: []clause.Column{{Name: "character_id"}, {Name: "person_id"}},
DoUpdates: clause.AssignmentColumns([]string{"language"}),
}).Create(&entities.CharacterVoiceActor{
- CharacterID: char.ID,
- VoiceActorID: va.ID,
- Language: cva.Language,
+ CharacterID: char.ID,
+ PersonID: va.ID,
+ Language: cva.Language,
})
}
@@ -104,7 +104,7 @@ func GetAnimeCharacters[T idType](maptype enums.MappingType, id T) ([]entities.C
if err := DB.First(&char, row.CharacterID).Error; err != nil {
continue
}
- DB.Preload("VoiceActor").Where("character_id = ?", char.ID).Find(&char.VoiceActors)
+ DB.Preload("Person").Where("character_id = ?", char.ID).Find(&char.VoiceActors)
char.Role = row.Role
characters = append(characters, char)
}
@@ -128,7 +128,7 @@ func GetAnimeCharacter[T idType](maptype enums.MappingType, id T, characterMALID
var char entities.Character
if err := DB.
- Preload("VoiceActors.VoiceActor").
+ Preload("VoiceActors.Person").
Preload("AnimeAppearances").
Where("mal_id = ?", characterMALID).
First(&char).Error; err != nil {
@@ -164,7 +164,7 @@ func loadAnimeCharacters(anime *entities.Anime) {
if err := DB.First(&char, row.CharacterID).Error; err != nil {
continue
}
- DB.Preload("VoiceActor").
+ DB.Preload("Person").
Where("character_id = ?", char.ID).
Find(&char.VoiceActors)
char.Role = row.Role
@@ -194,7 +194,7 @@ func SaveAnimeCharacters(animeID uint, characters []entities.Character) error {
})
for _, cva := range char.VoiceActors {
- va := cva.VoiceActor
+ va := cva.Person
if va == nil {
continue
}
@@ -208,12 +208,12 @@ func SaveAnimeCharacters(animeID uint, characters []entities.Character) error {
}
DB.Clauses(clause.OnConflict{
- Columns: []clause.Column{{Name: "character_id"}, {Name: "voice_actor_id"}},
+ Columns: []clause.Column{{Name: "character_id"}, {Name: "person_id"}},
DoUpdates: clause.AssignmentColumns([]string{"language"}),
}).Create(&entities.CharacterVoiceActor{
- CharacterID: char.ID,
- VoiceActorID: va.ID,
- Language: cva.Language,
+ CharacterID: char.ID,
+ PersonID: va.ID,
+ Language: cva.Language,
})
}
}
@@ -224,3 +224,172 @@ func SetCharacterEnriched(malID int) error {
now := time.Now()
return DB.Model(&entities.Character{}).Where("mal_id = ?", malID).Update("enriched_at", now).Error
}
+
+func GetCharacterByMALID(malID int) (entities.Character, error) {
+ var char entities.Character
+ if err := DB.
+ Preload("VoiceActors.Person").
+ Preload("AnimeAppearances").
+ Where("mal_id = ?", malID).
+ First(&char).Error; err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return entities.Character{}, errors.New("character not found")
+ }
+ return entities.Character{}, err
+ }
+ return char, nil
+}
+
+func GetAllPersonStubs() ([]personStub, error) {
+ var stubs []personStub
+ if err := DB.Model(&entities.Person{}).Select("mal_id, enriched_at").Scan(&stubs).Error; err != nil {
+ return nil, err
+ }
+ return stubs, nil
+}
+
+func UpdatePersonDetails(
+ malID int,
+ url, websiteURL, image, name, givenName, familyName string,
+ alternateNames []string,
+ birthday *time.Time,
+ favorites int,
+ about string,
+ voiceRoles []entities.PersonVoiceRole,
+ animeCredits []entities.PersonAnimeCredit,
+ mangaCredits []entities.PersonMangaCredit,
+) error {
+ var p entities.Person
+ if err := DB.Where("mal_id = ?", malID).First(&p).Error; err != nil {
+ return err
+ }
+
+ p.URL = url
+ p.WebsiteURL = websiteURL
+ p.Image = image
+ p.Name = name
+ p.GivenName = givenName
+ p.FamilyName = familyName
+ p.AlternateNames = alternateNames
+ p.Birthday = birthday
+ p.Favorites = favorites
+ p.About = about
+
+ if err := DB.Save(&p).Error; err != nil {
+ 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)
+ }
+
+ 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)
+ }
+
+ 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)
+ }
+
+ return nil
+}
+
+func SetPersonEnriched(malID int) error {
+ now := time.Now()
+ return DB.Model(&entities.Person{}).Where("mal_id = ?", malID).Update("enriched_at", now).Error
+}
+
+func GetAnimePeople[T idType](maptype enums.MappingType, id T) ([]entities.Person, error) {
+ mapping, err := GetAnimeMapping(maptype, id)
+ if err != nil {
+ return nil, errors.New("anime not found")
+ }
+
+ var anime entities.Anime
+ if err := DB.Where("mapping_id = ?", mapping.ID).Select("id").First(&anime).Error; err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return nil, errors.New("anime not found")
+ }
+ return nil, errors.New("anime not found")
+ }
+
+ var charRows []struct {
+ CharacterID uint
+ }
+ DB.Table("anime_characters").
+ Select("character_id").
+ Where("anime_id = ?", anime.ID).
+ Scan(&charRows)
+
+ personMap := make(map[uint]*entities.Person)
+ personChars := make(map[uint][]entities.PersonCharacterEntry)
+
+ for _, row := range charRows {
+ var char entities.Character
+ if err := DB.First(&char, row.CharacterID).Error; err != 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,
+ })
+ }
+ }
+
+ result := make([]entities.Person, 0, len(personMap))
+ for pID, p := range personMap {
+ p.Characters = personChars[pID]
+ result = append(result, *p)
+ }
+ return result, nil
+}
+
+func GetPerson(malID int) (entities.Person, error) {
+ var p entities.Person
+ if err := DB.
+ Preload("VoiceRoles").
+ Preload("AnimeCredits").
+ Preload("MangaCredits").
+ Where("mal_id = ?", malID).
+ First(&p).Error; err != nil {
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return entities.Person{}, errors.New("person not found")
+ }
+ return entities.Person{}, err
+ }
+ return p, nil
+}
diff --git a/repositories/types.go b/repositories/types.go
index d83fa11..afa7056 100644
--- a/repositories/types.go
+++ b/repositories/types.go
@@ -27,3 +27,8 @@ type characterStub struct {
MALID int
EnrichedAt *time.Time
}
+
+type personStub struct {
+ MALID int
+ EnrichedAt *time.Time
+}
diff --git a/router/router.go b/router/router.go
index 05356c3..9e2b2bd 100644
--- a/router/router.go
+++ b/router/router.go
@@ -16,7 +16,13 @@ func Initialize(router *fiber.App) {
animeRouter.Get("/:id/episodes", controllers.GetAnimeEpisodes)
animeRouter.Get("/:id/episodes/:episodeId", controllers.GetAnimeEpisode)
animeRouter.Get("/:id/characters", controllers.GetAnimeCharacters)
- animeRouter.Get("/:id/characters/:characterId", controllers.GetAnimeCharacter)
+ animeRouter.Get("/:id/people", controllers.GetAnimePeople)
+
+ characterRouter := router.Group("/character")
+ characterRouter.Get("/:characterId", controllers.GetAnimeCharacter)
+
+ peopleRouter := router.Group("/people")
+ peopleRouter.Get("/:personId", controllers.GetPerson)
// Anime routes
// animeRouter := router.Group("/a")
diff --git a/services/anime.go b/services/anime.go
index a53f725..787943b 100644
--- a/services/anime.go
+++ b/services/anime.go
@@ -338,10 +338,7 @@ func applyJikanData(anime *entities.Anime, jikanAnime *types.JikanAnimeResponse,
for _, va := range jc.VoiceActors {
char.VoiceActors = append(char.VoiceActors, entities.CharacterVoiceActor{
Language: va.Language,
- VoiceActor: &entities.VoiceActor{
- MALID: va.Person.MALID,
- Name: va.Person.Name,
- URL: va.Person.URL,
+ Person: &entities.Person{
Image: va.Person.Images.JPG.ImageURL,
},
})
diff --git a/tasks/charactersync.task.go b/tasks/charactersync.task.go
index 0ba578e..762c2f3 100644
--- a/tasks/charactersync.task.go
+++ b/tasks/charactersync.task.go
@@ -53,12 +53,7 @@ func CharacterSync() error {
for _, v := range d.Voices {
voiceActors = append(voiceActors, entities.CharacterVoiceActor{
Language: v.Language,
- VoiceActor: &entities.VoiceActor{
- MALID: v.Person.MALID,
- Name: v.Person.Name,
- URL: v.Person.URL,
- Image: v.Person.Images.JPG.ImageURL,
- },
+ Person: &entities.Person{},
})
}
diff --git a/tasks/personasync.task.go b/tasks/personasync.task.go
new file mode 100644
index 0000000..3d4f6ac
--- /dev/null
+++ b/tasks/personasync.task.go
@@ -0,0 +1,134 @@
+package tasks
+
+import (
+ "fmt"
+ "metachan/entities"
+ "metachan/repositories"
+ "metachan/utils/api/jikan"
+ "metachan/utils/logger"
+ "time"
+)
+
+func ResumePersonEnrichment() {
+ go PersonSync()
+}
+
+func PersonSync() error {
+ stubs, err := repositories.GetAllPersonStubs()
+ if err != nil {
+ return fmt.Errorf("failed to load person stubs: %w", err)
+ }
+
+ sevenDaysAgo := time.Now().Add(-7 * 24 * time.Hour)
+
+ hasWork := false
+ for _, s := range stubs {
+ if s.EnrichedAt == nil || !s.EnrichedAt.After(sevenDaysAgo) {
+ hasWork = true
+ break
+ }
+ }
+ if !hasWork {
+ return nil
+ }
+
+ total := len(stubs)
+ startTime := time.Now()
+ enriched := 0
+
+ for i, s := range stubs {
+ if s.EnrichedAt != nil && s.EnrichedAt.After(sevenDaysAgo) {
+ continue
+ }
+
+ resp, err := jikan.GetPersonByMALID(s.MALID)
+ if err != nil {
+ logger.Warnf("PersonSync", "Failed to fetch person %d: %v", s.MALID, err)
+ continue
+ }
+
+ d := resp.Data
+
+ var birthday *time.Time
+ if d.Birthday != nil && *d.Birthday != "" {
+ layouts := []string{
+ time.RFC3339,
+ "2006-01-02T15:04:05-07:00",
+ "2006-01-02",
+ }
+ for _, layout := range layouts {
+ if t, err := time.Parse(layout, *d.Birthday); err == nil {
+ birthday = &t
+ break
+ }
+ }
+ }
+
+ var websiteURL string
+ if d.WebsiteURL != nil {
+ websiteURL = *d.WebsiteURL
+ }
+
+ var voiceRoles []entities.PersonVoiceRole
+ for _, v := range d.Voices {
+ voiceRoles = append(voiceRoles, entities.PersonVoiceRole{
+ Role: v.Role,
+ AnimeMALID: v.Anime.MALID,
+ AnimeTitle: v.Anime.Title,
+ AnimeURL: v.Anime.URL,
+ AnimeImageURL: v.Anime.Images.JPG.ImageURL,
+ CharacterMALID: v.Character.MALID,
+ CharacterName: v.Character.Name,
+ CharacterURL: v.Character.URL,
+ CharacterImageURL: v.Character.Images.JPG.ImageURL,
+ })
+ }
+
+ var animeCredits []entities.PersonAnimeCredit
+ for _, a := range d.Anime {
+ animeCredits = append(animeCredits, entities.PersonAnimeCredit{
+ Position: a.Position,
+ AnimeMALID: a.Anime.MALID,
+ AnimeTitle: a.Anime.Title,
+ AnimeURL: a.Anime.URL,
+ AnimeImageURL: a.Anime.Images.JPG.ImageURL,
+ })
+ }
+
+ var mangaCredits []entities.PersonMangaCredit
+ for _, m := range d.Manga {
+ mangaCredits = append(mangaCredits, entities.PersonMangaCredit{
+ Position: m.Position,
+ MangaMALID: m.Manga.MALID,
+ MangaTitle: m.Manga.Title,
+ MangaURL: m.Manga.URL,
+ MangaImageURL: m.Manga.Images.JPG.ImageURL,
+ })
+ }
+
+ if err := repositories.UpdatePersonDetails(
+ d.MALID,
+ d.URL, websiteURL, d.Images.JPG.ImageURL,
+ d.Name, d.GivenName, d.FamilyName,
+ d.AlternateNames, birthday,
+ d.Favorites, d.About,
+ voiceRoles, animeCredits, mangaCredits,
+ ); err != nil {
+ logger.Warnf("PersonSync", "Failed to update person %d: %v", s.MALID, err)
+ continue
+ }
+
+ if err := repositories.SetPersonEnriched(d.MALID); err != nil {
+ logger.Warnf("PersonSync", "Failed to stamp enriched_at for person %d: %v", d.MALID, err)
+ }
+
+ enriched++
+ if (i+1)%50 == 0 || (i+1) == total {
+ progress, eta := calculateProgress(i+1, total, startTime)
+ logger.Infof("PersonSync", "Enriching: %d/%d (%.1f%%) | ETA: %v", i+1, total, progress, eta)
+ }
+ }
+
+ logger.Successf("PersonSync", "Background enrichment complete. Enriched %d people", enriched)
+ return nil
+}
diff --git a/tasks/tasks.go b/tasks/tasks.go
index 707a522..9df67fc 100644
--- a/tasks/tasks.go
+++ b/tasks/tasks.go
@@ -18,7 +18,6 @@ func init() {
Mutex: sync.Mutex{},
}
- // Register ProducerSync task (every 7 days) - runs first to populate unified producer table
err := GlobalTaskManager.RegisterTask(types.Task{
Name: "ProducerSync",
Interval: 7 * 24 * time.Hour,
@@ -30,7 +29,6 @@ func init() {
logger.Errorf("TaskManager", "Failed to register ProducerSync task: %v", err)
}
- // Register GenreSync task (every 7 days)
err = GlobalTaskManager.RegisterTask(types.Task{
Name: "GenreSync",
Interval: 7 * 24 * time.Hour,
@@ -41,8 +39,6 @@ func init() {
logger.Errorf("TaskManager", "Failed to register GenreSync task: %v", err)
}
- // Register AniFetch task (weekly) - fetches anime mappings from Fribb list
- // Depends on ProducerSync and GenreSync completing first
err = GlobalTaskManager.RegisterTask(types.Task{
Name: "AnimeFetch",
Interval: 7 * 24 * time.Hour,
@@ -54,7 +50,6 @@ func init() {
logger.Errorf("TaskManager", "Failed to register AnimeFetch task: %v", err)
}
- // Register AnimeSync task (runs after AnimeFetch completes) - only if enabled in config
if config.Sync.AniSync {
err = GlobalTaskManager.RegisterTask(types.Task{
Name: "AnimeSync",
@@ -78,9 +73,19 @@ func init() {
if err != nil {
logger.Errorf("TaskManager", "Failed to register CharacterSync task: %v", err)
}
+
+ err = GlobalTaskManager.RegisterTask(types.Task{
+ Name: "PersonSync",
+ Interval: 0,
+ Execute: PersonSync,
+ OnResume: ResumePersonEnrichment,
+ Dependencies: []string{"CharacterSync"},
+ })
+ if err != nil {
+ logger.Errorf("TaskManager", "Failed to register PersonSync task: %v", err)
+ }
}
- // Register AnimeUpdate task (every 15 minutes)
err = GlobalTaskManager.RegisterTask(types.Task{
Name: "AnimeUpdate",
Interval: 15 * time.Minute,
diff --git a/types/jikan.go b/types/jikan.go
index 3f574af..45434d3 100644
--- a/types/jikan.go
+++ b/types/jikan.go
@@ -246,3 +246,40 @@ type JikanProducersResponse struct {
type JikanSingleProducerResponse struct {
Data JikanSingleProducer `json:"data"`
}
+
+type JikanPersonVoiceRole struct {
+ Role string `json:"role"`
+ Anime JikanCharacterSimpleAnime `json:"anime"`
+ Character JikanCharacterPerson `json:"character"`
+}
+
+type JikanPersonAnimeCredit struct {
+ Position string `json:"position"`
+ Anime JikanCharacterSimpleAnime `json:"anime"`
+}
+
+type JikanPersonMangaCredit struct {
+ Position string `json:"position"`
+ Manga JikanCharacterSimpleAnime `json:"manga"`
+}
+
+type JikanFullPersonData struct {
+ MALID int `json:"mal_id"`
+ URL string `json:"url"`
+ WebsiteURL *string `json:"website_url"`
+ Images JikanGenericImageEntity `json:"images"`
+ Name string `json:"name"`
+ GivenName string `json:"given_name"`
+ FamilyName string `json:"family_name"`
+ AlternateNames []string `json:"alternate_names"`
+ Birthday *string `json:"birthday"`
+ Favorites int `json:"favorites"`
+ About string `json:"about"`
+ Anime []JikanPersonAnimeCredit `json:"anime"`
+ Manga []JikanPersonMangaCredit `json:"manga"`
+ Voices []JikanPersonVoiceRole `json:"voices"`
+}
+
+type JikanPersonFullResponse struct {
+ Data JikanFullPersonData `json:"data"`
+}
diff --git a/utils/api/jikan/jikan.go b/utils/api/jikan/jikan.go
index 507130a..c5bb2c8 100644
--- a/utils/api/jikan/jikan.go
+++ b/utils/api/jikan/jikan.go
@@ -335,3 +335,22 @@ func GetCharacterByMALID(id int) (*types.JikanCharacterFullResponse, error) {
}
return &response, nil
}
+
+func GetPersonByMALID(id int) (*types.JikanPersonFullResponse, error) {
+ url := fmt.Sprintf("%s/people/%d/full", jikanAPIBaseURL, id)
+ ctx, cancel := context.WithTimeout(context.Background(), contextTimeout)
+ defer cancel()
+
+ bytes, err := clientInstance.makeRequest(ctx, url)
+ if err != nil {
+ logger.Errorf("JikanClient", "GetPersonByMALID failed for ID %d: %v", id, err)
+ return nil, errors.New("failed to fetch person data from Jikan API")
+ }
+
+ var response types.JikanPersonFullResponse
+ if err := json.Unmarshal(bytes, &response); err != nil {
+ logger.Errorf("JikanClient", "Failed to unmarshal person response for ID %d: %v", id, err)
+ return nil, errors.New("failed to parse person data from Jikan API")
+ }
+ return &response, nil
+}