feat: add comments and cleanup unused imports across codebase
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetAnimeCharacters returns character list for an anime with voice actor info.
|
||||
func (c *Client) GetAnimeCharacters(ctx context.Context, id int) ([]CharacterEntry, error) {
|
||||
url := fmt.Sprintf("%s/anime/%d/characters", c.baseURL, id)
|
||||
cacheKey := fmt.Sprintf("anime:characters:%d", id)
|
||||
@@ -18,6 +19,7 @@ func (c *Client) GetAnimeCharacters(ctx context.Context, id int) ([]CharacterEnt
|
||||
return resp.Data, nil
|
||||
}
|
||||
|
||||
// GetAnimeRecommendations returns user-submitted recommendations for an anime.
|
||||
func (c *Client) GetAnimeRecommendations(ctx context.Context, id int) ([]RecommendationEntry, error) {
|
||||
url := fmt.Sprintf("%s/anime/%d/recommendations", c.baseURL, id)
|
||||
cacheKey := fmt.Sprintf("anime:recommendations:%d", id)
|
||||
@@ -30,6 +32,7 @@ func (c *Client) GetAnimeRecommendations(ctx context.Context, id int) ([]Recomme
|
||||
return resp.Data, nil
|
||||
}
|
||||
|
||||
// GetAnimeByID returns full anime details; finished series cached 30 days, airing cached 1 day.
|
||||
func (c *Client) GetAnimeByID(ctx context.Context, id int) (Anime, error) {
|
||||
cacheKey := fmt.Sprintf("anime:%d", id)
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ type Client struct {
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
db db.Querier
|
||||
retrySignal chan struct{}
|
||||
retrySignal chan struct{} // signals retry worker to process queued retries
|
||||
mu sync.Mutex
|
||||
lastReqTime time.Time
|
||||
lastReqTime time.Time // rate limiting: last request timestamp
|
||||
}
|
||||
|
||||
func NewClient(db db.Querier) *Client {
|
||||
@@ -51,6 +51,7 @@ func (e *APIError) Error() string {
|
||||
return fmt.Sprintf("jikan api returned status %d", e.StatusCode)
|
||||
}
|
||||
|
||||
// IsNotFoundError returns true if the error is an APIError with 404 status.
|
||||
func IsNotFoundError(err error) bool {
|
||||
var apiErr *APIError
|
||||
if errors.As(err, &apiErr) {
|
||||
@@ -60,6 +61,7 @@ func IsNotFoundError(err error) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsRetryableError returns true if the error should trigger a retry.
|
||||
func IsRetryableError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
@@ -90,6 +92,7 @@ func isRetryableStatus(statusCode int) bool {
|
||||
return statusCode >= 500 && statusCode <= 504
|
||||
}
|
||||
|
||||
// retryDelay returns exponential backoff delay: 500ms, 1s, 2s, 4s, 8s (capped).
|
||||
func retryDelay(attempt int) time.Duration {
|
||||
base := 500 * time.Millisecond
|
||||
delay := base * time.Duration(1<<attempt)
|
||||
@@ -100,6 +103,7 @@ func retryDelay(attempt int) time.Duration {
|
||||
return delay
|
||||
}
|
||||
|
||||
// parseRetryAfter parses Retry-After header value (seconds) into duration.
|
||||
func parseRetryAfter(value string) (time.Duration, bool) {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
@@ -138,6 +142,7 @@ func truncateErrorMessage(message string) string {
|
||||
return message[:400]
|
||||
}
|
||||
|
||||
// notifyRetryWorker signals the retry worker, non-blocking.
|
||||
func (c *Client) notifyRetryWorker() {
|
||||
select {
|
||||
case c.retrySignal <- struct{}{}:
|
||||
@@ -145,10 +150,12 @@ func (c *Client) notifyRetryWorker() {
|
||||
}
|
||||
}
|
||||
|
||||
// RetrySignal returns channel that signals when retries are enqueued.
|
||||
func (c *Client) RetrySignal() <-chan struct{} {
|
||||
return c.retrySignal
|
||||
}
|
||||
|
||||
// EnqueueAnimeFetchRetry queues a failed anime fetch for later retry if the error is retryable.
|
||||
func (c *Client) EnqueueAnimeFetchRetry(parentCtx context.Context, animeID int, cause error) {
|
||||
if animeID <= 0 || !IsRetryableError(cause) {
|
||||
return
|
||||
@@ -168,6 +175,7 @@ func (c *Client) EnqueueAnimeFetchRetry(parentCtx context.Context, animeID int,
|
||||
c.notifyRetryWorker()
|
||||
}
|
||||
|
||||
// waitRateLimit enforces Jikan's 3 req/sec rate limit with 400ms spacing.
|
||||
func (c *Client) waitRateLimit(ctx context.Context) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
@@ -193,6 +201,7 @@ func (c *Client) waitRateLimit(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCache retrieves cached data by key, returns true on cache hit.
|
||||
func (c *Client) getCache(parentCtx context.Context, key string, out any) bool {
|
||||
ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
|
||||
defer cancel()
|
||||
@@ -206,6 +215,7 @@ func (c *Client) getCache(parentCtx context.Context, key string, out any) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// getStaleCache retrieves expired-but-available cache by key.
|
||||
func (c *Client) getStaleCache(parentCtx context.Context, key string, out any) bool {
|
||||
ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
|
||||
defer cancel()
|
||||
@@ -219,6 +229,7 @@ func (c *Client) getStaleCache(parentCtx context.Context, key string, out any) b
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// setCache stores data in cache with specified TTL.
|
||||
func (c *Client) setCache(parentCtx context.Context, key string, data any, ttl time.Duration) {
|
||||
ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
|
||||
defer cancel()
|
||||
@@ -240,6 +251,7 @@ type cacheResult struct {
|
||||
hasStale bool
|
||||
}
|
||||
|
||||
// isEmptyResult detects if response contains no meaningful data.
|
||||
func isEmptyResult(out any) bool {
|
||||
switch v := out.(type) {
|
||||
case *TopAnimeResponse:
|
||||
@@ -254,6 +266,7 @@ func isEmptyResult(out any) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// getWithCache fetches URL with cache-aside pattern: checks cache first, falls back to stale on error.
|
||||
func (c *Client) getWithCache(ctx context.Context, cacheKey string, ttl time.Duration, url string, out any) error {
|
||||
if c.getCache(ctx, cacheKey, out) {
|
||||
if !isEmptyResult(out) {
|
||||
@@ -289,6 +302,7 @@ func (c *Client) getWithCache(ctx context.Context, cacheKey string, ttl time.Dur
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchWithRetry makes HTTP request with exponential backoff retry on transient failures.
|
||||
func (c *Client) fetchWithRetry(ctx context.Context, urlStr string, out any) error {
|
||||
maxRetries := 5
|
||||
for attempt := range maxRetries {
|
||||
|
||||
@@ -2,5 +2,5 @@ package jikan
|
||||
|
||||
import "time"
|
||||
|
||||
const shortCacheTTL = time.Hour
|
||||
const longCacheTTL = time.Hour * 24
|
||||
const shortCacheTTL = time.Hour // 1 hour - for frequently changing data
|
||||
const longCacheTTL = time.Hour * 24 // 24 hours - for stable data like genres
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetEpisodes returns episode list for a specific page.
|
||||
func (c *Client) GetEpisodes(ctx context.Context, animeID int, page int) (EpisodesResponse, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
@@ -20,6 +21,7 @@ func (c *Client) GetEpisodes(ctx context.Context, animeID int, page int) (Episod
|
||||
return result, err
|
||||
}
|
||||
|
||||
// GetAllEpisodes fetches all pages of episodes in parallel and flattens results.
|
||||
func (c *Client) GetAllEpisodes(ctx context.Context, animeID int) ([]Episode, error) {
|
||||
// First page to get total pages
|
||||
first, err := c.GetEpisodes(ctx, animeID, 1)
|
||||
@@ -67,6 +69,7 @@ func (c *Client) GetAllEpisodes(ctx context.Context, animeID int) ([]Episode, er
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetEpisodesRange fetches episodes from startPage to endPage sequentially.
|
||||
func (c *Client) GetEpisodesRange(ctx context.Context, animeID int, startPage, endPage int) ([]Episode, error) {
|
||||
var all []Episode
|
||||
for page := startPage; page <= endPage; page++ {
|
||||
|
||||
@@ -14,10 +14,12 @@ import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// chiaki.watchOrderURL is the external watch order tool used for relation ordering.
|
||||
const chiakiWatchOrderURL = "https://chiaki.site/?/tools/watch_order/id/%d"
|
||||
const watchOrderCacheTTL = time.Hour * 24
|
||||
const maxWatchOrderEntries = 120
|
||||
const maxWatchOrderEntries = 120 // cap to prevent huge relation chains
|
||||
|
||||
// watchOrderTypeLabel normalizes watch order type to display-friendly labels.
|
||||
func watchOrderTypeLabel(value string) string {
|
||||
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||
switch normalized {
|
||||
@@ -30,6 +32,7 @@ func watchOrderTypeLabel(value string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// isAllowedWatchOrderType returns true only for TV and Movie types (filters out specials, etc).
|
||||
func isAllowedWatchOrderType(value string) bool {
|
||||
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||
return normalized == "tv" || normalized == "movie"
|
||||
@@ -39,6 +42,7 @@ func relationCacheKey(id int) string {
|
||||
return fmt.Sprintf("relations:watch-order:%d", id)
|
||||
}
|
||||
|
||||
// getWatchOrder fetches watch order from chiaki, caches result for 24h.
|
||||
func (c *Client) getWatchOrder(ctx context.Context, id int) (watchorder.WatchOrderResult, error) {
|
||||
cacheKey := relationCacheKey(id)
|
||||
|
||||
@@ -81,6 +85,7 @@ func (c *Client) getWatchOrder(ctx context.Context, id int) (watchorder.WatchOrd
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// currentOnlyRelation returns just the current anime when watch order lookup fails.
|
||||
func (c *Client) currentOnlyRelation(ctx context.Context, id int) ([]RelationEntry, error) {
|
||||
currentAnime, err := c.GetAnimeByID(ctx, id)
|
||||
if err != nil {
|
||||
@@ -95,6 +100,7 @@ func (c *Client) currentOnlyRelation(ctx context.Context, id int) ([]RelationEnt
|
||||
}}, nil
|
||||
}
|
||||
|
||||
// GetFullRelations returns related anime based on watch order, with parallel fetching (3 concurrent).
|
||||
func (c *Client) GetFullRelations(ctx context.Context, id int) ([]RelationEntry, error) {
|
||||
result, err := c.getWatchOrder(ctx, id)
|
||||
if err != nil {
|
||||
|
||||
@@ -8,10 +8,12 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Search performs a basic anime search by query string.
|
||||
func (c *Client) Search(ctx context.Context, query string, page int) (SearchResult, error) {
|
||||
return c.search(ctx, query, page, 0)
|
||||
}
|
||||
|
||||
// SearchAdvanced performs a filtered anime search with type, status, ordering, and genre filters.
|
||||
func (c *Client) SearchAdvanced(ctx context.Context, query, animeType, status, orderBy, sort string, genres []int, sfw bool, page, limit int) (SearchResult, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
@@ -122,6 +124,7 @@ func (c *Client) search(ctx context.Context, query string, page int, limit int)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetTopAnime returns the top-rated anime list for a given page.
|
||||
func (c *Client) GetTopAnime(ctx context.Context, page int) (TopAnimeResult, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
@@ -145,6 +148,7 @@ func (c *Client) GetTopAnime(ctx context.Context, page int) (TopAnimeResult, err
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAnimeGenres returns list of all anime genres, cached long-term.
|
||||
func (c *Client) GetAnimeGenres(ctx context.Context) ([]Genre, error) {
|
||||
const cacheKey = "anime_genres"
|
||||
|
||||
|
||||
@@ -8,9 +8,10 @@ import (
|
||||
|
||||
type ScheduleResult struct {
|
||||
Animes []Anime
|
||||
HasNextPage bool
|
||||
HasNextPage bool // whether more pages available
|
||||
}
|
||||
|
||||
// GetSchedule returns anime airing on a specific day of the week.
|
||||
func (c *Client) GetSchedule(ctx context.Context, day string) (ScheduleResult, error) {
|
||||
day = strings.ToLower(day)
|
||||
cacheKey := fmt.Sprintf("schedule_%s", day)
|
||||
@@ -29,6 +30,7 @@ func (c *Client) GetSchedule(ctx context.Context, day string) (ScheduleResult, e
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetFullSchedule returns anime airing schedule for all seven days.
|
||||
func (c *Client) GetFullSchedule(ctx context.Context) (map[string][]Anime, error) {
|
||||
days := []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}
|
||||
schedule := make(map[string][]Anime)
|
||||
@@ -44,6 +46,7 @@ func (c *Client) GetFullSchedule(ctx context.Context) (map[string][]Anime, error
|
||||
return schedule, nil
|
||||
}
|
||||
|
||||
// GetSeasonsNow returns currently airing anime for the current season.
|
||||
func (c *Client) GetSeasonsNow(ctx context.Context, page int) (TopAnimeResult, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
@@ -64,6 +67,7 @@ func (c *Client) GetSeasonsNow(ctx context.Context, page int) (TopAnimeResult, e
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetSeasonsUpcoming returns anime scheduled to air in upcoming seasons.
|
||||
func (c *Client) GetSeasonsUpcoming(ctx context.Context, page int) (TopAnimeResult, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
@@ -84,6 +88,7 @@ func (c *Client) GetSeasonsUpcoming(ctx context.Context, page int) (TopAnimeResu
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetRandomAnime returns a random anime from the database.
|
||||
func (c *Client) GetRandomAnime(ctx context.Context) (Anime, error) {
|
||||
var result struct {
|
||||
Data Anime `json:"data"`
|
||||
|
||||
@@ -28,6 +28,7 @@ type ProducerResponse struct {
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// GetAnimeByProducer returns anime list for a producer/studio, includes producer name.
|
||||
func (c *Client) GetAnimeByProducer(ctx context.Context, producerID int, page int) (StudioAnimeResult, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
@@ -75,6 +76,7 @@ func (c *Client) GetAnimeByProducer(ctx context.Context, producerID int, page in
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GetProducerByID returns full producer/studio details.
|
||||
func (c *Client) GetProducerByID(ctx context.Context, producerID int) (ProducerResponse, error) {
|
||||
cacheKey := fmt.Sprintf("producer:info:%d", producerID)
|
||||
|
||||
|
||||
@@ -155,18 +155,22 @@ type RecommendationsResponse struct {
|
||||
Data []RecommendationEntry `json:"data"`
|
||||
}
|
||||
|
||||
// ScoredByFormatted returns formatted count (e.g. "1 234 567").
|
||||
func (a Anime) ScoredByFormatted() string {
|
||||
return formatNumber(a.ScoredBy)
|
||||
}
|
||||
|
||||
// MembersFormatted returns formatted count (e.g. "1 234 567").
|
||||
func (a Anime) MembersFormatted() string {
|
||||
return formatNumber(a.Members)
|
||||
}
|
||||
|
||||
// FavoritesFormatted returns formatted count (e.g. "1 234 567").
|
||||
func (a Anime) FavoritesFormatted() string {
|
||||
return formatNumber(a.Favorites)
|
||||
}
|
||||
|
||||
// formatNumber adds space separators to a number (1234567 -> "1 234 567").
|
||||
func formatNumber(n int) string {
|
||||
if n == 0 {
|
||||
return ""
|
||||
@@ -180,10 +184,12 @@ func formatNumber(n int) string {
|
||||
return strings.Join(res, " ")
|
||||
}
|
||||
|
||||
// ImageURL returns the webp large image URL for the anime.
|
||||
func (a Anime) ImageURL() string {
|
||||
return a.Images.Webp.LargeImageURL
|
||||
}
|
||||
|
||||
// ShortRating extracts just the rating code (e.g. "PG-13") from full rating string.
|
||||
func (a Anime) ShortRating() string {
|
||||
if a.Rating == "" {
|
||||
return ""
|
||||
@@ -197,6 +203,7 @@ func (a Anime) ShortRating() string {
|
||||
return a.Rating
|
||||
}
|
||||
|
||||
// ShortDuration extracts numeric duration in minutes (e.g. "23m" from "23 min per ep").
|
||||
func (a Anime) ShortDuration() string {
|
||||
if a.Duration == "" {
|
||||
return ""
|
||||
@@ -216,6 +223,7 @@ func (a Anime) ShortDuration() string {
|
||||
return a.Duration
|
||||
}
|
||||
|
||||
// DurationSeconds converts duration string to total seconds (e.g. "1 hr 30 min" -> 5400).
|
||||
func (a Anime) DurationSeconds() float64 {
|
||||
if a.Duration == "" {
|
||||
return 0
|
||||
@@ -256,6 +264,7 @@ func (a Anime) DurationSeconds() float64 {
|
||||
return float64(hours*60+minutes) * 60
|
||||
}
|
||||
|
||||
// Premiered returns formatted premiere string (e.g. "Winter 2020").
|
||||
func (a Anime) Premiered() string {
|
||||
if a.Season != "" && a.Year > 0 {
|
||||
return fmt.Sprintf("%s %d", seasonLabel(a.Season), a.Year)
|
||||
@@ -263,6 +272,7 @@ func (a Anime) Premiered() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// seasonLabel normalizes season string to title case (fall/autumn -> Fall).
|
||||
func seasonLabel(season string) string {
|
||||
switch strings.ToLower(season) {
|
||||
case "winter":
|
||||
@@ -356,6 +366,7 @@ type RelationEntry struct {
|
||||
IsExtra bool
|
||||
}
|
||||
|
||||
// DisplayTitle returns English title if available, otherwise Japanese, then default.
|
||||
func (a Anime) DisplayTitle() string {
|
||||
if a.TitleEnglish != "" {
|
||||
return a.TitleEnglish
|
||||
|
||||
@@ -15,7 +15,9 @@ import (
|
||||
|
||||
const defaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
|
||||
|
||||
// idPattern extracts the watch order ID from chiaki.site URLs
|
||||
var idPattern = regexp.MustCompile(`/id/(\d+)`)
|
||||
// malLinkPattern extracts MAL IDs from watch order entries
|
||||
var malLinkPattern = regexp.MustCompile(`myanimelist\.net/anime/(\d+)`)
|
||||
|
||||
var ErrInvalidWatchOrderURL = errors.New("invalid watch order url")
|
||||
@@ -46,10 +48,10 @@ func (e *HTTPStatusError) Error() string {
|
||||
}
|
||||
|
||||
type WatchOrderEntry struct {
|
||||
ID int `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
TitleAlt string `json:"title_alt,omitempty"`
|
||||
ID int `json:"id"` // MAL anime ID
|
||||
Type string `json:"type"` // anime type label (e.g. "TV", "Movie")
|
||||
Title string `json:"title"` // primary title
|
||||
TitleAlt string `json:"title_alt,omitempty"` // alternative title
|
||||
}
|
||||
|
||||
type WatchOrderResult struct {
|
||||
@@ -106,6 +108,7 @@ func fetchDocument(ctx context.Context, httpClient *http.Client, url string) (*g
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
// limit body read for error context; avoid reading large error pages
|
||||
body, _ := io.ReadAll(io.LimitReader(response.Body, 512))
|
||||
return nil, &HTTPStatusError{
|
||||
StatusCode: response.StatusCode,
|
||||
@@ -198,6 +201,8 @@ func hasWatchOrderTable(doc *goquery.Document) bool {
|
||||
return doc.Find("#wo_list").Length() > 0
|
||||
}
|
||||
|
||||
// shouldTryProxy returns true for transient errors where the Jina proxy may help
|
||||
// (e.g. Cloudflare blocking, rate limits)
|
||||
func shouldTryProxy(err error) bool {
|
||||
var statusError *HTTPStatusError
|
||||
if errors.As(err, &statusError) {
|
||||
@@ -243,6 +248,8 @@ func fetchProxyText(ctx context.Context, httpClient *http.Client, url string) (s
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
// parseJinaEntries parses Jina proxy output, which contains one line per entry
|
||||
// in format: "title | type | https://myanimelist.net/anime/ID"
|
||||
func parseJinaEntries(text string) []WatchOrderEntry {
|
||||
lines := strings.Split(text, "\n")
|
||||
entries := make([]WatchOrderEntry, 0)
|
||||
@@ -312,6 +319,8 @@ func isNoiseTitleLine(value string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// titleFromContext looks backward from metaIndex to find the actual title lines.
|
||||
// It skips noise lines (URLs, metadata prefixes, etc.) and returns (primary, alt).
|
||||
func titleFromContext(lines []string, metaIndex int) (string, string) {
|
||||
collected := make([]string, 0, 2)
|
||||
|
||||
@@ -340,6 +349,7 @@ func titleFromContext(lines []string, metaIndex int) (string, string) {
|
||||
return collected[0], ""
|
||||
}
|
||||
|
||||
// reversed order: older lines first -> title, newer -> alt
|
||||
return collected[1], collected[0]
|
||||
}
|
||||
|
||||
@@ -357,6 +367,8 @@ func fetchViaProxy(ctx context.Context, httpClient *http.Client, url string, roo
|
||||
return WatchOrderResult{ID: rootID, WatchOrder: entries}, nil
|
||||
}
|
||||
|
||||
// FetchWatchOrder fetches the watch order from chiaki.site.
|
||||
// Falls back to the Jina proxy if the site is blocked or returns an empty table.
|
||||
func FetchWatchOrder(ctx context.Context, httpClient *http.Client, url string) (WatchOrderResult, error) {
|
||||
rootID, err := parseRootID(url)
|
||||
if err != nil {
|
||||
@@ -371,6 +383,7 @@ func FetchWatchOrder(ctx context.Context, httpClient *http.Client, url string) (
|
||||
return WatchOrderResult{}, err
|
||||
}
|
||||
|
||||
// empty table indicates JS-rendered content; need proxy
|
||||
if !hasWatchOrderTable(doc) {
|
||||
return fetchViaProxy(ctx, httpClient, url, rootID)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user