@@ -3,6 +3,7 @@ package playback
33
44import (
55 "fmt"
6+ "slices"
67 "strconv"
78 "strings"
89 "sync"
@@ -61,13 +62,17 @@ func (nav *AllAnimeNavigator) GetNextEpisode(currentEpisode string) (string, err
6162 return "" , fmt .Errorf ("invalid current episode number: %w" , err )
6263 }
6364
65+ // Validate against the actual episode list instead of assuming a
66+ // contiguous 1..N numbering: lists starting at "0" (or with gaps) made
67+ // the old len()-based check accept phantom episodes past the last one.
6468 next := current + 1
65- if next > len (nav .episodes ) {
69+ target := strconv .Itoa (next )
70+ if ! slices .Contains (nav .episodes , target ) {
6671 return "" , fmt .Errorf ("no next episode available (current: %d, total: %d)" , current , len (nav .episodes ))
6772 }
6873
6974 util .Debugf ("Navigating to next episode from=%d to=%d" , current , next )
70- return strconv . Itoa ( next ) , nil
75+ return target , nil
7176}
7277
7378// GetPreviousEpisode returns the previous episode before the current one
@@ -77,13 +82,16 @@ func (nav *AllAnimeNavigator) GetPreviousEpisode(currentEpisode string) (string,
7782 return "" , fmt .Errorf ("invalid current episode number: %w" , err )
7883 }
7984
85+ // Same list-membership check as GetNextEpisode: allows "0" when the
86+ // source really has an episode 0 and rejects anything not in the list.
8087 prev := current - 1
81- if prev < 1 {
88+ target := strconv .Itoa (prev )
89+ if ! slices .Contains (nav .episodes , target ) {
8290 return "" , fmt .Errorf ("no previous episode available (current: %d)" , current )
8391 }
8492
8593 util .Debugf ("Navigating to previous episode from=%d to=%d" , current , prev )
86- return strconv . Itoa ( prev ) , nil
94+ return target , nil
8795}
8896
8997// GetTotalEpisodes returns the total number of episodes
@@ -94,30 +102,51 @@ func (nav *AllAnimeNavigator) GetTotalEpisodes() int {
94102// ListAllEpisodes returns all available episode numbers
95103func (nav * AllAnimeNavigator ) ListAllEpisodes () []string {
96104 result := make ([]string , len (nav .episodes ))
97- for i := range nav .episodes {
98- result [i ] = strconv .Itoa (i + 1 )
99- }
105+ copy (result , nav .episodes )
100106 return result
101107}
102108
103109// Helper function to check if anime is from AllAnime source
104110func isAllAnimeSource (anime * models.Anime ) bool {
105- if anime .Source == "AllAnime" {
106- return true
111+ // An explicit source is authoritative. The length heuristic below used to
112+ // run even when Source was set, misrouting SuperFlix/SFlix (short numeric
113+ // TMDB IDs as URLs) into AllAnime navigation.
114+ if anime .Source != "" {
115+ return anime .Source == "AllAnime"
107116 }
108117
109118 if strings .Contains (anime .URL , "allanime" ) {
110119 return true
111120 }
112121
113- // Check if URL is a short ID (AllAnime typically uses short IDs)
114- if len (anime .URL ) < 30 &&
115- strings .ContainsAny (anime .URL , "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" ) &&
116- ! strings .Contains (anime .URL , "http" ) {
117- return true
122+ // AllAnime IDs are short opaque alphanumeric tokens containing at least
123+ // one letter (purely numeric IDs belong to other sources).
124+ return anime .URL != "" &&
125+ len (anime .URL ) < 30 &&
126+ isAlphanumericID (anime .URL ) &&
127+ strings .ContainsAny (anime .URL , "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" )
128+ }
129+
130+ // isAlphanumericID reports whether s is non-empty and contains only ASCII
131+ // letters and digits.
132+ func isAlphanumericID (s string ) bool {
133+ for _ , r := range s {
134+ isAlnum := ('a' <= r && r <= 'z' ) || ('A' <= r && r <= 'Z' ) || ('0' <= r && r <= '9' )
135+ if ! isAlnum {
136+ return false
137+ }
118138 }
139+ return s != ""
140+ }
119141
120- return false
142+ // allAnimePathWords are URL path segments that precede the anime ID in full
143+ // AllAnime URLs and must never be mistaken for the ID itself.
144+ var allAnimePathWords = map [string ]bool {
145+ "anime" : true ,
146+ "bangumi" : true ,
147+ "watch" : true ,
148+ "serie" : true ,
149+ "series" : true ,
121150}
122151
123152// Helper function to extract AllAnime ID from URL
@@ -127,12 +156,27 @@ func extractAllAnimeID(url string) string {
127156 return url
128157 }
129158
130- // Extract ID from full AllAnime URLs if needed
159+ // Extract the ID from full AllAnime URLs. The old scan returned the first
160+ // "long alphanumeric" segment, which for any https URL was the scheme
161+ // ("https:") — poisoning the navigator cache key and every episode fetch.
131162 if strings .Contains (url , "allanime" ) {
132- parts := strings .SplitSeq (url , "/" )
133- for part := range parts {
134- if len (part ) > 5 && len (part ) < 30 &&
135- strings .ContainsAny (part , "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" ) {
163+ parts := strings .Split (url , "/" )
164+
165+ // Prefer the segment right after a known path word (/anime/<id>/...)
166+ for i , part := range parts {
167+ if allAnimePathWords [part ] && i + 1 < len (parts ) &&
168+ len (parts [i + 1 ]) < 30 && isAlphanumericID (parts [i + 1 ]) {
169+ return parts [i + 1 ]
170+ }
171+ }
172+
173+ // Fallback: first plausible ID segment, skipping scheme and domain
174+ for _ , part := range parts {
175+ if part == "" || strings .HasSuffix (part , ":" ) ||
176+ strings .Contains (part , "." ) || allAnimePathWords [part ] {
177+ continue
178+ }
179+ if len (part ) > 5 && len (part ) < 30 && isAlphanumericID (part ) {
136180 return part
137181 }
138182 }
0 commit comments