fix(scraper): replace browserless with direct HTTP and fix presign 404 race
- Fix audio presign 404: MinIO upload is now synchronous before response is sent, eliminating the race where presign was called before the file landed in MinIO - Replace all Browserless usage with direct HTTP client across catalogue, metadata, ranking, and browse — novelfire.net pages are server-rendered and don't need a headless browser; direct is faster and more reliable - Harden handleBrowse with 3-attempt retry loop, proper backoff, and full browser-like headers to reduce 502s from novelfire.net bot detection - Remove Browserless env vars (BROWSERLESS_URL/TOKEN/STRATEGY) from main.go; add SCRAPER_TIMEOUT as a single timeout knob - Clean up now-dead rejectResourceTypes var and Browserless-specific WaitFor/ RejectResourceTypes/GotoOptions fields from scraper calls
This commit is contained in:
@@ -30,25 +30,6 @@ const (
|
||||
rankingPath = "/genre-all/sort-popular/status-all/all-novel"
|
||||
)
|
||||
|
||||
// rejectResourceTypes lists Browserless resource types to block on every request.
|
||||
// We keep: document (the page), script (JS renders the DOM), fetch/xhr (JS data calls).
|
||||
// Everything else is safe to drop for HTML-only scraping.
|
||||
var rejectResourceTypes = []string{
|
||||
"cspviolationreport",
|
||||
"eventsource",
|
||||
"fedcm",
|
||||
"font",
|
||||
"image",
|
||||
"manifest",
|
||||
"media",
|
||||
"other",
|
||||
"ping",
|
||||
"signedexchange",
|
||||
"stylesheet",
|
||||
"texttrack",
|
||||
"websocket",
|
||||
}
|
||||
|
||||
// RankingStore is the subset of storage.Store consumed by ScrapeRanking.
|
||||
type RankingStore interface {
|
||||
WriteRankingItem(ctx context.Context, item scraper.RankingItem) error
|
||||
@@ -56,19 +37,19 @@ type RankingStore interface {
|
||||
}
|
||||
|
||||
// Scraper is the novelfire.net implementation of scraper.NovelScraper.
|
||||
// It uses the /content strategy by default (rendered HTML via Browserless).
|
||||
// It uses direct HTTP requests (no headless browser required).
|
||||
type Scraper struct {
|
||||
client browser.BrowserClient
|
||||
urlClient browser.BrowserClient // separate client for URL retrieval (uses browserless content strategy)
|
||||
chapterClient browser.BrowserClient // direct HTTP client for chapter text (no JS rendering needed)
|
||||
urlClient browser.BrowserClient // used for chapter list pagination
|
||||
chapterClient browser.BrowserClient // used for chapter text fetching
|
||||
rankingStore RankingStore
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// New returns a new novelfire Scraper.
|
||||
// client is used for catalogue/metadata/ranking fetching (Browserless).
|
||||
// client is used for catalogue/metadata/ranking fetching (direct HTTP).
|
||||
// urlClient is used for chapter list pagination; falls back to client if nil.
|
||||
// chapterClient is used for chapter text fetching (plain HTTP); falls back to client if nil.
|
||||
// chapterClient is used for chapter text fetching; falls back to client if nil.
|
||||
// rankingStore is optional; pass nil to disable freshness checks and per-item persistence.
|
||||
func New(client browser.BrowserClient, log *slog.Logger, urlClient browser.BrowserClient, chapterClient browser.BrowserClient, rankingStore RankingStore) *Scraper {
|
||||
if log == nil {
|
||||
@@ -108,18 +89,9 @@ func (s *Scraper) ScrapeCatalogue(ctx context.Context) (<-chan scraper.Catalogue
|
||||
}
|
||||
|
||||
s.log.Info("scraping catalogue page", "page", page, "url", pageURL)
|
||||
s.log.Debug("catalogue page fetch starting",
|
||||
"page", page,
|
||||
"payload_url", pageURL,
|
||||
"payload_wait_selector", ".novel-item",
|
||||
"payload_wait_selector_timeout_ms", 5000,
|
||||
)
|
||||
|
||||
html, err := s.client.GetContent(ctx, browser.ContentRequest{
|
||||
URL: pageURL,
|
||||
WaitFor: &browser.WaitForSelector{Selector: ".novel-item", Timeout: 5000},
|
||||
RejectResourceTypes: rejectResourceTypes,
|
||||
GotoOptions: &browser.GotoOptions{Timeout: 60000},
|
||||
URL: pageURL,
|
||||
})
|
||||
if err != nil {
|
||||
s.log.Debug("catalogue page fetch failed",
|
||||
@@ -212,17 +184,10 @@ func (s *Scraper) ScrapeCatalogue(ctx context.Context) (<-chan scraper.Catalogue
|
||||
// ─── MetadataProvider ────────────────────────────────────────────────────────
|
||||
|
||||
func (s *Scraper) ScrapeMetadata(ctx context.Context, bookURL string) (scraper.BookMeta, error) {
|
||||
s.log.Debug("metadata fetch starting",
|
||||
"payload_url", bookURL,
|
||||
"payload_wait_selector", ".novel-title",
|
||||
"payload_wait_selector_timeout_ms", 5000,
|
||||
)
|
||||
s.log.Debug("metadata fetch starting", "url", bookURL)
|
||||
|
||||
raw, err := s.client.GetContent(ctx, browser.ContentRequest{
|
||||
URL: bookURL,
|
||||
WaitFor: &browser.WaitForSelector{Selector: ".novel-title", Timeout: 5000},
|
||||
RejectResourceTypes: rejectResourceTypes,
|
||||
GotoOptions: &browser.GotoOptions{Timeout: 60000},
|
||||
URL: bookURL,
|
||||
})
|
||||
if err != nil {
|
||||
s.log.Debug("metadata fetch failed", "url", bookURL, "err", err)
|
||||
@@ -498,10 +463,7 @@ func (s *Scraper) ScrapeRanking(ctx context.Context, maxPages int) (<-chan scrap
|
||||
|
||||
s.log.Info("scraping popular ranking page", "page", page, "url", pageURL)
|
||||
raw, err := s.client.GetContent(ctx, browser.ContentRequest{
|
||||
URL: pageURL,
|
||||
WaitFor: &browser.WaitForSelector{Selector: ".novel-item", Timeout: 5000},
|
||||
RejectResourceTypes: rejectResourceTypes,
|
||||
GotoOptions: &browser.GotoOptions{Timeout: 60000},
|
||||
URL: pageURL,
|
||||
})
|
||||
if err != nil {
|
||||
s.log.Debug("ranking page fetch failed", "page", page, "url", pageURL, "err", err)
|
||||
|
||||
Reference in New Issue
Block a user