package latest import ( "context" "errors" "fmt" ) // seriesRead carries the two facts the poll and the acquirer both extract // from a series page. Persistence, stamps and scheduling stay with the // callers, so the policies that keep the two flows distinct (stamp order, // cooldowns) are not swallowed by the module. type seriesRead struct { Latest latestChapter HasLatest bool Cover string HasCover bool // BodyLen is the fetched body's length, surfaced because the no-chapter // log uses it to tell a markup change from a body the size cap cut short. BodyLen int } // errNotFetchable and errNoFetcher separate the gate and the route from fetch // failures so each caller keeps its own distinct log line for all three. var ( errNotFetchable = errors.New("series url not fetchable") errNoFetcher = errors.New("no fetcher for site") ) // readSeriesPage performs the series-page read the poll and the acquirer have // in common: gate the address, choose the route, fetch the page, extract the // Latest Chapter and the Cover address. It persists nothing and stamps // nothing. // // series_url arrives in a client-supplied PUT body (PUT /bookmarks/{key} // accepts any string), so the gate is not an optimisation against burning a // request on an unknown site: without it, the server would issue a GET from // its own network position to whatever URL a token-holder writes, including // link-local/internal addresses or non-https schemes. func readSeriesPage(ctx context.Context, site, seriesURL string, browser, tls Fetcher) (seriesRead, error) { if !fetchableSeriesURL(site, seriesURL) { return seriesRead{}, fmt.Errorf("%w: site=%q url=%q", errNotFetchable, site, seriesURL) } f := fetcherFor(site, browser, tls) if f == nil { return seriesRead{}, fmt.Errorf("%w: site %q", errNoFetcher, site) } body, status, err := f.Get(ctx, seriesURL) if err != nil { return seriesRead{}, fmt.Errorf("fetch %s: %w", seriesURL, err) } if status != 200 { return seriesRead{}, fmt.Errorf("fetch %s: status %d", seriesURL, status) } latest, hasLatest := latestChapterFrom(site, seriesURL, body) cover, hasCover := coverFrom(site, seriesURL, body) return seriesRead{Latest: latest, HasLatest: hasLatest, Cover: cover, HasCover: hasCover, BodyLen: len(body)}, nil }