diff options
| author | Bobby <[email protected]> | 2026-02-24 17:36:55 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2026-02-24 17:36:55 +0530 |
| commit | 2df69fab61b580b6b329db214ee0025a9d84958d (patch) | |
| tree | bf7d69e1cfc5f6dc3e387f99325e6842e7dc60c1 | |
| parent | d3507ae5b9d88a250b444c0e996fa07f84f6e3c5 (diff) | |
| download | metachan-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.go | 39 | ||||
| -rw-r--r-- | database/migrate.go | 22 | ||||
| -rw-r--r-- | entities/persona.go | 66 | ||||
| -rw-r--r-- | repositories/persona.go | 195 | ||||
| -rw-r--r-- | repositories/types.go | 5 | ||||
| -rw-r--r-- | router/router.go | 8 | ||||
| -rw-r--r-- | services/anime.go | 5 | ||||
| -rw-r--r-- | tasks/charactersync.task.go | 7 | ||||
| -rw-r--r-- | tasks/personasync.task.go | 134 | ||||
| -rw-r--r-- | tasks/tasks.go | 17 | ||||
| -rw-r--r-- | types/jikan.go | 37 | ||||
| -rw-r--r-- | utils/api/jikan/jikan.go | 19 |
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 +} |
