247 lines
5.4 KiB
Go
247 lines
5.4 KiB
Go
package jikan
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var canonicalRelationOrder = []string{
|
|
"prequel",
|
|
"sequel",
|
|
"parent story",
|
|
"full story",
|
|
"alternative version",
|
|
"alternative setting",
|
|
}
|
|
|
|
var extraRelationOrder = []string{
|
|
"side story",
|
|
"spin-off",
|
|
"summary",
|
|
"other",
|
|
}
|
|
|
|
var relationPriorityOrder = append(
|
|
append([]string{}, canonicalRelationOrder...),
|
|
extraRelationOrder...,
|
|
)
|
|
|
|
func relationKey(rel string) string {
|
|
key := strings.ToLower(strings.TrimSpace(rel))
|
|
key = strings.ReplaceAll(key, "_", " ")
|
|
key = strings.Join(strings.Fields(key), " ")
|
|
|
|
switch key {
|
|
case "prequels":
|
|
return "prequel"
|
|
case "sequels":
|
|
return "sequel"
|
|
case "side stories":
|
|
return "side story"
|
|
case "spin off", "spinoff":
|
|
return "spin-off"
|
|
default:
|
|
return key
|
|
}
|
|
}
|
|
|
|
func relationLabel(rel string) string {
|
|
key := relationKey(rel)
|
|
switch key {
|
|
case "prequel":
|
|
return "Prequels"
|
|
case "sequel":
|
|
return "Sequels"
|
|
case "parent story":
|
|
return "Parent story"
|
|
case "full story":
|
|
return "Full story"
|
|
case "alternative version":
|
|
return "Alternative version"
|
|
case "alternative setting":
|
|
return "Alternative setting"
|
|
case "side story":
|
|
return "Side story"
|
|
case "spin-off":
|
|
return "Spin-off"
|
|
case "summary":
|
|
return "Summary"
|
|
case "other":
|
|
return "Other"
|
|
default:
|
|
return strings.TrimSpace(rel)
|
|
}
|
|
}
|
|
|
|
func isCanonicalRelation(rel string) bool {
|
|
return slices.Contains(canonicalRelationOrder, relationKey(rel))
|
|
}
|
|
|
|
func isExtraRelation(rel string) bool {
|
|
return slices.Contains(extraRelationOrder, relationKey(rel))
|
|
}
|
|
|
|
func isFranchiseRelation(rel string) bool {
|
|
return isCanonicalRelation(rel) || isExtraRelation(rel)
|
|
}
|
|
|
|
func relationOrder(rel string) int {
|
|
key := relationKey(rel)
|
|
for i, allowed := range relationPriorityOrder {
|
|
if key == allowed {
|
|
return i
|
|
}
|
|
}
|
|
return len(relationPriorityOrder) + 1
|
|
}
|
|
|
|
func relationAiredAt(anime Anime) (time.Time, bool) {
|
|
from := strings.TrimSpace(anime.Aired.From)
|
|
if from != "" {
|
|
if parsed, err := time.Parse(time.RFC3339, from); err == nil {
|
|
return parsed, true
|
|
}
|
|
if parsed, err := time.Parse("2006-01-02", from); err == nil {
|
|
return parsed, true
|
|
}
|
|
}
|
|
|
|
if anime.Year > 0 {
|
|
return time.Date(anime.Year, time.January, 1, 0, 0, 0, 0, time.UTC), true
|
|
}
|
|
|
|
return time.Time{}, false
|
|
}
|
|
|
|
func sortRelationEntriesChronological(entries []RelationEntry) {
|
|
sort.SliceStable(entries, func(i int, j int) bool {
|
|
left := entries[i]
|
|
right := entries[j]
|
|
|
|
leftAiredAt, leftHasAiredAt := relationAiredAt(left.Anime)
|
|
rightAiredAt, rightHasAiredAt := relationAiredAt(right.Anime)
|
|
|
|
if leftHasAiredAt != rightHasAiredAt {
|
|
return leftHasAiredAt
|
|
}
|
|
|
|
if leftHasAiredAt && !leftAiredAt.Equal(rightAiredAt) {
|
|
return leftAiredAt.Before(rightAiredAt)
|
|
}
|
|
|
|
leftRelationOrder := relationOrder(left.Relation)
|
|
rightRelationOrder := relationOrder(right.Relation)
|
|
if leftRelationOrder != rightRelationOrder {
|
|
return leftRelationOrder < rightRelationOrder
|
|
}
|
|
|
|
leftTitle := strings.ToLower(left.Anime.DisplayTitle())
|
|
rightTitle := strings.ToLower(right.Anime.DisplayTitle())
|
|
if leftTitle != rightTitle {
|
|
return leftTitle < rightTitle
|
|
}
|
|
|
|
return left.Anime.MalID < right.Anime.MalID
|
|
})
|
|
}
|
|
|
|
func relationEntries(ctx context.Context, c *Client, anime Anime) ([]RelationEntry, error) {
|
|
entries := make([]RelationEntry, 0)
|
|
|
|
for _, group := range anime.Relations {
|
|
if !isFranchiseRelation(group.Relation) {
|
|
continue
|
|
}
|
|
|
|
for _, entry := range group.Entry {
|
|
if entry.Type != "anime" {
|
|
continue
|
|
}
|
|
|
|
relAnime, err := c.GetAnimeByID(ctx, entry.MalID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
entries = append(entries, RelationEntry{
|
|
Anime: relAnime,
|
|
Relation: relationLabel(group.Relation),
|
|
IsCurrent: false,
|
|
IsExtra: !isCanonicalRelation(group.Relation),
|
|
})
|
|
}
|
|
}
|
|
|
|
return entries, nil
|
|
}
|
|
|
|
func relationMap(ctx context.Context, c *Client, id int) (map[int]RelationEntry, error) {
|
|
currentAnime, err := c.GetAnimeByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := map[int]RelationEntry{
|
|
currentAnime.MalID: {
|
|
Anime: currentAnime,
|
|
Relation: "Current",
|
|
IsCurrent: true,
|
|
IsExtra: false,
|
|
},
|
|
}
|
|
|
|
queue := []Anime{currentAnime}
|
|
visited := map[int]bool{currentAnime.MalID: true}
|
|
|
|
for len(queue) > 0 {
|
|
anime := queue[0]
|
|
queue = queue[1:]
|
|
|
|
entries, err := relationEntries(ctx, c, anime)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, rel := range entries {
|
|
existing, exists := result[rel.Anime.MalID]
|
|
if !exists {
|
|
result[rel.Anime.MalID] = rel
|
|
} else if !existing.IsCurrent {
|
|
if existing.IsExtra && !rel.IsExtra {
|
|
// Prefer canonical timeline links over extras when both point to the same anime.
|
|
result[rel.Anime.MalID] = rel
|
|
} else if existing.IsExtra && rel.IsExtra && relationOrder(rel.Relation) < relationOrder(existing.Relation) {
|
|
// Keep the most specific extra label when multiple extra relations exist.
|
|
result[rel.Anime.MalID] = rel
|
|
}
|
|
}
|
|
|
|
if !rel.IsExtra && !visited[rel.Anime.MalID] {
|
|
visited[rel.Anime.MalID] = true
|
|
queue = append(queue, rel.Anime)
|
|
}
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Client) GetFullRelations(ctx context.Context, id int) ([]RelationEntry, error) {
|
|
relationByID, err := relationMap(ctx, c, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ordered := make([]RelationEntry, 0, len(relationByID))
|
|
for _, entry := range relationByID {
|
|
ordered = append(ordered, entry)
|
|
}
|
|
|
|
sortRelationEntriesChronological(ordered)
|
|
|
|
return ordered, nil
|
|
}
|