feat: add watch order mode toggle
This commit is contained in:
@@ -20,6 +20,22 @@ const chiakiWatchOrderURL = "https://chiaki.site/?/tools/watch_order/id/%d"
|
||||
const watchOrderCacheTTL = time.Hour * 24
|
||||
const maxWatchOrderEntries = 120 // cap to prevent huge relation chains
|
||||
|
||||
type WatchOrderMode string
|
||||
|
||||
const (
|
||||
WatchOrderModeMain WatchOrderMode = "main"
|
||||
WatchOrderModeComplete WatchOrderMode = "complete"
|
||||
)
|
||||
|
||||
func NormalizeWatchOrderMode(value string) WatchOrderMode {
|
||||
switch WatchOrderMode(strings.ToLower(strings.TrimSpace(value))) {
|
||||
case WatchOrderModeComplete:
|
||||
return WatchOrderModeComplete
|
||||
default:
|
||||
return WatchOrderModeMain
|
||||
}
|
||||
}
|
||||
|
||||
// watchOrderTypeLabel normalizes watch order type to display-friendly labels.
|
||||
func watchOrderTypeLabel(value string) string {
|
||||
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||
@@ -28,17 +44,35 @@ func watchOrderTypeLabel(value string) string {
|
||||
return "TV"
|
||||
case "movie":
|
||||
return "Movie"
|
||||
case "ona":
|
||||
return "ONA"
|
||||
case "ova":
|
||||
return "OVA"
|
||||
default:
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
|
||||
// isAllowedWatchOrderType returns true only for TV and Movie types (filters out specials, etc).
|
||||
func isTVWatchOrderType(value string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(value), "tv")
|
||||
}
|
||||
|
||||
// isAllowedWatchOrderType returns true for the default uncluttered watch order types.
|
||||
func isAllowedWatchOrderType(value string) bool {
|
||||
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||
return normalized == "tv" || normalized == "movie"
|
||||
}
|
||||
|
||||
func hasTVWatchOrderEntry(entries []watchorder.WatchOrderEntry) bool {
|
||||
for _, entry := range entries {
|
||||
if isTVWatchOrderType(entry.Type) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func relationCacheKey(id int) string {
|
||||
return fmt.Sprintf("relations:watch-order:%d", id)
|
||||
}
|
||||
@@ -166,15 +200,19 @@ func (c *Client) handleWatchOrderError(ctx context.Context, id int, err error) (
|
||||
return c.currentOnlyRelation(ctx, id)
|
||||
}
|
||||
|
||||
func buildAllowedWatchOrderEntries(result watchorder.WatchOrderResult) ([]watchorder.WatchOrderEntry, map[int]bool) {
|
||||
func buildAllowedWatchOrderEntries(result watchorder.WatchOrderResult, mode WatchOrderMode) ([]watchorder.WatchOrderEntry, map[int]bool) {
|
||||
allowedEntries := make([]watchorder.WatchOrderEntry, 0, len(result.WatchOrder))
|
||||
seen := make(map[int]bool)
|
||||
shouldIncludeAllTypes := mode == WatchOrderModeComplete || !hasTVWatchOrderEntry(result.WatchOrder)
|
||||
|
||||
for _, entry := range result.WatchOrder {
|
||||
if len(allowedEntries) >= maxWatchOrderEntries {
|
||||
break
|
||||
}
|
||||
if !isAllowedWatchOrderType(entry.Type) || seen[entry.ID] {
|
||||
if seen[entry.ID] {
|
||||
continue
|
||||
}
|
||||
if !shouldIncludeAllTypes && !isAllowedWatchOrderType(entry.Type) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -267,13 +305,13 @@ type fetchResult struct {
|
||||
}
|
||||
|
||||
// GetFullRelations returns related anime based on watch order, with parallel fetching (3 concurrent).
|
||||
func (c *Client) GetFullRelations(ctx context.Context, id int) ([]RelationEntry, error) {
|
||||
func (c *Client) GetFullRelations(ctx context.Context, id int, mode WatchOrderMode) ([]RelationEntry, error) {
|
||||
result, err := c.getWatchOrder(ctx, id)
|
||||
if err != nil {
|
||||
return c.handleWatchOrderError(ctx, id, err)
|
||||
}
|
||||
|
||||
allowedEntries, seen := buildAllowedWatchOrderEntries(result)
|
||||
allowedEntries, seen := buildAllowedWatchOrderEntries(result, mode)
|
||||
fetched := c.fetchRelationResults(ctx, allowedEntries)
|
||||
relations := buildRelationsFromResults(fetched, id)
|
||||
relations, err = c.ensureCurrentRelation(ctx, id, seen, relations)
|
||||
@@ -290,6 +328,6 @@ func (c *Client) GetFullRelations(ctx context.Context, id int) ([]RelationEntry,
|
||||
|
||||
func (c *Client) WarmFullRelations(id int) {
|
||||
c.runAsyncRefresh(func(ctx context.Context) {
|
||||
_, _ = c.GetFullRelations(ctx, id)
|
||||
_, _ = c.GetFullRelations(ctx, id, WatchOrderModeMain)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package jikan
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"mal/integrations/watchorder"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func runBoolCases(t *testing.T, tests []struct {
|
||||
name string
|
||||
@@ -36,6 +39,138 @@ func TestIsAllowedWatchOrderType(t *testing.T) {
|
||||
runBoolCases(t, tests, isAllowedWatchOrderType)
|
||||
}
|
||||
|
||||
func TestNormalizeWatchOrderMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want WatchOrderMode
|
||||
}{
|
||||
{name: "empty defaults main", input: "", want: WatchOrderModeMain},
|
||||
{name: "main", input: "main", want: WatchOrderModeMain},
|
||||
{name: "complete", input: "complete", want: WatchOrderModeComplete},
|
||||
{name: "case and whitespace", input: " COMPLETE ", want: WatchOrderModeComplete},
|
||||
{name: "unknown defaults main", input: "everything", want: WatchOrderModeMain},
|
||||
}
|
||||
|
||||
for _, testCase := range tests {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
got := NormalizeWatchOrderMode(testCase.input)
|
||||
if got != testCase.want {
|
||||
t.Fatalf("expected %q, got %q", testCase.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasTVWatchOrderEntry(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
entries []watchorder.WatchOrderEntry
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "contains tv",
|
||||
entries: []watchorder.WatchOrderEntry{
|
||||
{ID: 1, Type: "Movie"},
|
||||
{ID: 2, Type: " TV "},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "ona only",
|
||||
entries: []watchorder.WatchOrderEntry{
|
||||
{ID: 1, Type: "ONA"},
|
||||
{ID: 2, Type: "Special"},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range tests {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
got := hasTVWatchOrderEntry(testCase.entries)
|
||||
if got != testCase.want {
|
||||
t.Fatalf("expected %v, got %v", testCase.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAllowedWatchOrderEntriesKeepsDefaultTypesWhenTVExists(t *testing.T) {
|
||||
result := watchorder.WatchOrderResult{
|
||||
WatchOrder: []watchorder.WatchOrderEntry{
|
||||
{ID: 1, Type: "TV"},
|
||||
{ID: 2, Type: "Special"},
|
||||
{ID: 3, Type: "Movie"},
|
||||
{ID: 4, Type: "ONA"},
|
||||
},
|
||||
}
|
||||
|
||||
entries, seen := buildAllowedWatchOrderEntries(result, WatchOrderModeMain)
|
||||
if len(entries) != 2 {
|
||||
t.Fatalf("expected 2 entries, got %d", len(entries))
|
||||
}
|
||||
|
||||
if entries[0].ID != 1 || entries[1].ID != 3 {
|
||||
t.Fatalf("unexpected entries: %+v", entries)
|
||||
}
|
||||
|
||||
if !seen[1] || !seen[3] || seen[2] || seen[4] {
|
||||
t.Fatalf("unexpected seen map: %+v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAllowedWatchOrderEntriesIncludesAllTypesWhenNoTVExists(t *testing.T) {
|
||||
result := watchorder.WatchOrderResult{
|
||||
WatchOrder: []watchorder.WatchOrderEntry{
|
||||
{ID: 1, Type: "ONA"},
|
||||
{ID: 2, Type: "Special"},
|
||||
{ID: 3, Type: "Movie"},
|
||||
{ID: 1, Type: "ONA"},
|
||||
},
|
||||
}
|
||||
|
||||
entries, seen := buildAllowedWatchOrderEntries(result, WatchOrderModeMain)
|
||||
if len(entries) != 3 {
|
||||
t.Fatalf("expected 3 entries, got %d", len(entries))
|
||||
}
|
||||
|
||||
if entries[0].ID != 1 || entries[1].ID != 2 || entries[2].ID != 3 {
|
||||
t.Fatalf("unexpected entries: %+v", entries)
|
||||
}
|
||||
|
||||
if !seen[1] || !seen[2] || !seen[3] {
|
||||
t.Fatalf("unexpected seen map: %+v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAllowedWatchOrderEntriesIncludesAllTypesInCompleteMode(t *testing.T) {
|
||||
result := watchorder.WatchOrderResult{
|
||||
WatchOrder: []watchorder.WatchOrderEntry{
|
||||
{ID: 1, Type: "TV"},
|
||||
{ID: 2, Type: "Special"},
|
||||
{ID: 3, Type: "ONA"},
|
||||
{ID: 4, Type: "Movie"},
|
||||
},
|
||||
}
|
||||
|
||||
entries, seen := buildAllowedWatchOrderEntries(result, WatchOrderModeComplete)
|
||||
if len(entries) != 4 {
|
||||
t.Fatalf("expected 4 entries, got %d", len(entries))
|
||||
}
|
||||
|
||||
for index, entry := range entries {
|
||||
wantID := index + 1
|
||||
if entry.ID != wantID {
|
||||
t.Fatalf("expected entry %d to have id %d, got %+v", index, wantID, entry)
|
||||
}
|
||||
}
|
||||
|
||||
if !seen[1] || !seen[2] || !seen[3] || !seen[4] {
|
||||
t.Fatalf("unexpected seen map: %+v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchOrderTypeLabel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -44,6 +179,8 @@ func TestWatchOrderTypeLabel(t *testing.T) {
|
||||
}{
|
||||
{name: "tv", input: "tv", want: "TV"},
|
||||
{name: "movie", input: "movie", want: "Movie"},
|
||||
{name: "ona", input: "ona", want: "ONA"},
|
||||
{name: "ova", input: "ova", want: "OVA"},
|
||||
{name: "trimmed passthrough", input: " tv special ", want: "tv special"},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user