Skip to content
25 changes: 0 additions & 25 deletions anilist.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,26 +352,6 @@ func (c *AnilistClient) UpdateMangaEntry(
return nil
}

// ToggleFavouriteResponse represents the response from AniList ToggleFavourite mutation.
// The response contains nested "nodes" arrays matching the GraphQL query structure:
// anime { nodes { id } }, manga { nodes { id } }.
type ToggleFavouriteResponse struct {
Data struct {
ToggleFavourite struct {
Anime struct {
Nodes []struct {
ID int `json:"id"`
} `json:"nodes"`
} `json:"anime"`
Manga struct {
Nodes []struct {
ID int `json:"id"`
} `json:"nodes"`
} `json:"manga"`
} `json:"ToggleFavourite"`
} `json:"data"`
}

// ToggleFavourite toggles the favorite status of an anime or manga on AniList.
// Only one of animeID or mangaID should be provided (non-zero) per call.
// The ToggleFavourite mutation is idempotent: calling it on an already-favorited
Expand Down Expand Up @@ -437,11 +417,6 @@ func (c *AnilistClient) ToggleFavourite(ctx context.Context, animeID, mangaID in
return fmt.Errorf("GraphQL errors: %+v", graphqlResp.Errors)
}

var response ToggleFavouriteResponse
if err := json.Unmarshal(responseBody, &response); err != nil {
return fmt.Errorf("failed to unmarshal response: %w", err)
}

return nil
}

Expand Down
64 changes: 42 additions & 22 deletions anime.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"errors"
"fmt"
"log"
Expand Down Expand Up @@ -78,10 +79,11 @@ type Anime struct {
StartedAt *time.Time
FinishedAt *time.Time
IsFavourite bool
isReverse bool // true when used in reverse sync (MAL β†’ AniList)
}

func (a Anime) GetTargetID() TargetID {
if *reverseDirection {
if a.isReverse {
return TargetID(a.IDAnilist)
}
return TargetID(a.IDMal)
Expand All @@ -98,7 +100,7 @@ func (a Anime) GetMALID() TargetID {
}

func (a Anime) GetSourceID() int {
if *reverseDirection {
if a.isReverse {
return a.IDMal
}
return a.IDAnilist
Expand Down Expand Up @@ -166,7 +168,7 @@ func (a Anime) SameProgressWithTarget(t Target) bool {
return aa == bb
}

func (a Anime) SameTypeWithTarget(t Target) bool {
func (a Anime) SameTypeWithTarget(ctx context.Context, t Target) bool {
// Type assertion to ensure we're comparing with another Anime
b, ok := t.(Anime)
if !ok {
Expand All @@ -184,17 +186,17 @@ func (a Anime) SameTypeWithTarget(t Target) bool {
}

// Use the comprehensive title matching logic
return a.SameTitleWithTarget(t)
return a.SameTitleWithTarget(ctx, t)
}

func (a Anime) SameTitleWithTarget(t Target) bool {
func (a Anime) SameTitleWithTarget(ctx context.Context, t Target) bool {
b, ok := t.(Anime)
if !ok {
return false
}

// Check if titles match
if !titleMatchingLevels(
if !titleMatchingLevels(ctx,
a.TitleEN, a.TitleJP, a.TitleRomaji,
b.TitleEN, b.TitleJP, b.TitleRomaji,
) {
Expand Down Expand Up @@ -305,7 +307,7 @@ func (a Anime) GetTitle() string {
}

func (a Anime) String() string {
sb := strings.Builder{}
var sb strings.Builder
sb.WriteString("Anime{")
fmt.Fprintf(&sb, "IDAnilist: %d, ", a.IDAnilist)
fmt.Fprintf(&sb, "IDMal: %d, ", a.IDMal)
Expand All @@ -322,11 +324,15 @@ func (a Anime) String() string {
return sb.String()
}

func newAnimesFromMediaListGroups(groups []verniy.MediaListGroup, scoreFormat verniy.ScoreFormat) []Anime {
// newAnimesFromMediaListGroups converts AniList media list groups to domain Anime list.
// reverse=false: entries are forward-sync sources; reverse=true: reverse-sync targets.
func newAnimesFromMediaListGroups(
ctx context.Context, groups []verniy.MediaListGroup, scoreFormat verniy.ScoreFormat, reverse bool,
) []Anime {
res := make([]Anime, 0, len(groups))
for _, group := range groups {
for _, mediaList := range group.Entries {
a, err := newAnimeFromMediaListEntry(mediaList, scoreFormat)
a, err := newAnimeFromMediaListEntry(ctx, mediaList, scoreFormat, reverse)
if err != nil {
log.Printf("Error creating anime from media list entry: %v", err)
continue
Expand All @@ -338,7 +344,9 @@ func newAnimesFromMediaListGroups(groups []verniy.MediaListGroup, scoreFormat ve
return res
}

func newAnimeFromMediaListEntry(mediaList verniy.MediaList, scoreFormat verniy.ScoreFormat) (Anime, error) {
func newAnimeFromMediaListEntry(
ctx context.Context, mediaList verniy.MediaList, scoreFormat verniy.ScoreFormat, reverse bool,
) (Anime, error) {
if mediaList.Media == nil {
return Anime{}, errors.New("media is nil")
}
Expand All @@ -354,7 +362,7 @@ func newAnimeFromMediaListEntry(mediaList verniy.MediaList, scoreFormat verniy.S
var score int
if mediaList.Score != nil {
// Normalize AniList score to MAL format (0-10)
score = normalizeScoreForMAL(*mediaList.Score, scoreFormat)
score = normalizeScoreForMAL(ctx, *mediaList.Score, scoreFormat)
}

var progress int
Expand Down Expand Up @@ -414,13 +422,17 @@ func newAnimeFromMediaListEntry(mediaList verniy.MediaList, scoreFormat verniy.S
StartedAt: startedAt,
FinishedAt: finishedAt,
IsFavourite: isFavourite,
isReverse: reverse,
}, nil
}

func newAnimesFromMalAnimes(malAnimes []mal.Anime) []Anime {
// newAnimesFromMalAnimes converts MAL anime list to domain Anime list.
// reverse=false: entries are forward-sync targets (MAL IDs as target IDs).
// reverse=true: entries are reverse-sync sources (AniList IDs as target IDs).
func newAnimesFromMalAnimes(malAnimes []mal.Anime, reverse bool) []Anime {
res := make([]Anime, 0, len(malAnimes))
for _, malAnime := range malAnimes {
a, err := newAnimeFromMalAnime(malAnime)
a, err := newAnimeFromMalAnime(malAnime, reverse)
if err != nil {
log.Printf("failed to convert mal anime to anime: %v", err)
continue
Expand All @@ -430,10 +442,13 @@ func newAnimesFromMalAnimes(malAnimes []mal.Anime) []Anime {
return res
}

func newAnimesFromMalUserAnimes(malAnimes []mal.UserAnime) []Anime {
// newAnimesFromMalUserAnimes converts MAL user anime list to domain Anime list.
// reverse=false: entries are forward-sync targets (MAL IDs as target IDs).
// reverse=true: entries are reverse-sync sources (AniList IDs as target IDs).
func newAnimesFromMalUserAnimes(malAnimes []mal.UserAnime, reverse bool) []Anime {
res := make([]Anime, 0, len(malAnimes))
for _, malAnime := range malAnimes {
a, err := newAnimeFromMalAnime(malAnime.Anime)
a, err := newAnimeFromMalAnime(malAnime.Anime, reverse)
if err != nil {
log.Printf("failed to convert mal anime to anime: %v", err)
continue
Expand All @@ -447,7 +462,7 @@ func newAnimesFromMalUserAnimes(malAnimes []mal.UserAnime) []Anime {
return res
}

func newAnimeFromMalAnime(malAnime mal.Anime) (Anime, error) {
func newAnimeFromMalAnime(malAnime mal.Anime, reverse bool) (Anime, error) {
if malAnime.ID == 0 {
return Anime{}, errors.New("ID is nil")
}
Expand All @@ -465,10 +480,11 @@ func newAnimeFromMalAnime(malAnime mal.Anime) (Anime, error) {
titleJP = malAnime.AlternativeTitles.Ja
}

// In reverse sync mode, we need to leave AniList ID as 0 so the updater can find it by name
// In reverse sync, IDAnilist=0 triggers name-based search in the strategy chain.
// In forward sync, IDAnilist=-1 indicates "unknown" (MAL entries as targets don't need it).
anilistID := -1
if *reverseDirection {
anilistID = 0 // This will trigger name-based search in reverse sync
if reverse {
anilistID = 0
}

return Anime{
Expand All @@ -484,6 +500,7 @@ func newAnimeFromMalAnime(malAnime mal.Anime) (Anime, error) {
StartedAt: startedAt,
FinishedAt: finishedAt,
IsFavourite: false, // MAL API v2 does not provide favorites
isReverse: reverse,
}, nil
}

Expand Down Expand Up @@ -565,10 +582,12 @@ func newSourcesFromAnimes(animes []Anime) []Source {
return res
}

func newAnimesFromVerniyMedias(medias []verniy.Media) []Anime {
// newAnimesFromVerniyMedias converts AniList API search results to domain Anime list.
// reverse=true: entries will be used as reverse-sync targets (AniList IDs as target IDs).
func newAnimesFromVerniyMedias(medias []verniy.Media, reverse bool) []Anime {
res := make([]Anime, 0, len(medias))
for _, media := range medias {
a, err := newAnimeFromVerniyMedia(media)
a, err := newAnimeFromVerniyMedia(media, reverse)
if err != nil {
log.Printf("failed to convert verniy media to anime: %v", err)
continue
Expand All @@ -578,7 +597,7 @@ func newAnimesFromVerniyMedias(medias []verniy.Media) []Anime {
return res
}

func newAnimeFromVerniyMedia(media verniy.Media) (Anime, error) {
func newAnimeFromVerniyMedia(media verniy.Media, reverse bool) (Anime, error) {
if media.ID == 0 {
return Anime{}, errors.New("ID is 0")
}
Expand Down Expand Up @@ -627,6 +646,7 @@ func newAnimeFromVerniyMedia(media verniy.Media) (Anime, error) {
StartedAt: nil, // Will be set from MAL source
FinishedAt: nil, // Will be set from MAL source
IsFavourite: false, // Verniy media from search doesn't contain user favorite status
isReverse: reverse,
}, nil
}

Expand Down
Loading