- Switch ScrapeRanking to novelfire.net/genre-all/sort-popular URL and updated DOM selectors (div.novel-item, h3.novel-title, div.genres) - Replace 5 hardcoded refresh buttons with dynamic 100-page paginator (smart ellipsis via rankingPageNums) - Add RankingPageCacher interface and writer methods to cache raw HTML per page under static/books/_ranking_cache/page-N.html - ScrapeRanking serves from disk cache on hit and writes to cache on miss, skipping Browserless round-trip - Thread writer as PageCacher through novelfire.New and main.go - Add TestScrapeRanking_CacheHit and TestScrapeRanking_CacheMiss tests
145 lines
6.1 KiB
Go
145 lines
6.1 KiB
Go
// Package scraper defines the core interfaces and domain types for the libnovel
|
|
// scraping system. Each novel source implements these interfaces; the orchestrator
|
|
// wires them together without knowing anything about the concrete provider.
|
|
package scraper
|
|
|
|
import "context"
|
|
|
|
// ─── Domain types ────────────────────────────────────────────────────────────
|
|
|
|
// BookMeta carries all bibliographic information about a novel.
|
|
type BookMeta struct {
|
|
// Slug is a URL-safe identifier derived from the book title, e.g. "a-dragon-against-the-whole-world".
|
|
Slug string `yaml:"slug"`
|
|
// Title is the human-readable novel title.
|
|
Title string `yaml:"title"`
|
|
// Author of the novel.
|
|
Author string `yaml:"author"`
|
|
// Cover is an absolute URL to the cover image.
|
|
Cover string `yaml:"cover,omitempty"`
|
|
// Status is e.g. "Ongoing", "Completed".
|
|
Status string `yaml:"status,omitempty"`
|
|
// Genres is a list of genre tags.
|
|
Genres []string `yaml:"genres,omitempty"`
|
|
// Summary is the full description/synopsis text.
|
|
Summary string `yaml:"summary,omitempty"`
|
|
// TotalChapters is the total number of chapters known at scrape time.
|
|
TotalChapters int `yaml:"total_chapters,omitempty"`
|
|
// SourceURL is the canonical URL of the book's landing page.
|
|
SourceURL string `yaml:"source_url"`
|
|
// Ranking is the rank number from ranking pages.
|
|
Ranking int `yaml:"ranking,omitempty"`
|
|
}
|
|
|
|
// CatalogueEntry is a lightweight reference returned by CatalogueProvider.
|
|
type CatalogueEntry struct {
|
|
// Title is the novel title as shown in the catalogue listing.
|
|
Title string
|
|
// URL is the canonical landing-page URL of the novel.
|
|
URL string
|
|
}
|
|
|
|
// ChapterRef is a reference to a single chapter returned by ChapterListProvider.
|
|
type ChapterRef struct {
|
|
// Number is the 1-based chapter index within the book.
|
|
Number int
|
|
// Title is the chapter display title.
|
|
Title string
|
|
// URL is the full URL of the chapter page.
|
|
URL string
|
|
// Volume is an optional volume number (0 means no volume grouping).
|
|
Volume int
|
|
}
|
|
|
|
// Chapter contains the fully-extracted text of a single chapter.
|
|
type Chapter struct {
|
|
Ref ChapterRef
|
|
// Text is the plain / lightly-formatted chapter body (Markdown).
|
|
Text string
|
|
}
|
|
|
|
// ─── Scraping selector descriptors ───────────────────────────────────────────
|
|
|
|
// Selector describes how to locate an element in an HTML document.
|
|
// Exactly one of Tag, Class, or ID should be non-empty; when multiple are set
|
|
// they are combined (AND semantics).
|
|
type Selector struct {
|
|
// Tag is the HTML element name, e.g. "div", "p", "h1".
|
|
Tag string
|
|
// Class is one CSS class name (without the leading dot).
|
|
Class string
|
|
// ID is the element id attribute (without the leading #).
|
|
ID string
|
|
// Attr is an optional attribute name whose value should be extracted
|
|
// instead of the text content (e.g. "href", "src").
|
|
Attr string
|
|
// Multiple indicates that all matching elements should be collected,
|
|
// not just the first one.
|
|
Multiple bool
|
|
}
|
|
|
|
// ─── Provider interfaces ──────────────────────────────────────────────────────
|
|
|
|
// CatalogueProvider can enumerate every novel available on a source site.
|
|
// It handles pagination transparently and streams CatalogueEntry values.
|
|
type CatalogueProvider interface {
|
|
// ScrapeCatalogue pages through the entire catalogue, sending
|
|
// CatalogueEntry values to the returned channel. The channel is closed
|
|
// when all pages have been scraped or ctx is cancelled.
|
|
// Errors are surfaced via the error channel; a non-nil error does not
|
|
// necessarily terminate scraping.
|
|
ScrapeCatalogue(ctx context.Context) (<-chan CatalogueEntry, <-chan error)
|
|
}
|
|
|
|
// MetadataProvider can extract structured book metadata from a novel's landing page.
|
|
type MetadataProvider interface {
|
|
// ScrapeMetadata fetches and parses the metadata for the book at bookURL.
|
|
ScrapeMetadata(ctx context.Context, bookURL string) (BookMeta, error)
|
|
}
|
|
|
|
// ChapterListProvider can enumerate all chapters of a book from the chapter-list page.
|
|
type ChapterListProvider interface {
|
|
// ScrapeChapterList returns all chapter references for a book, ordered
|
|
// by chapter number ascending.
|
|
ScrapeChapterList(ctx context.Context, bookURL string) ([]ChapterRef, error)
|
|
}
|
|
|
|
// ChapterTextProvider can extract the readable text from a single chapter page.
|
|
type ChapterTextProvider interface {
|
|
// ScrapeChapterText fetches chapterURL and returns the chapter text as Markdown.
|
|
ScrapeChapterText(ctx context.Context, ref ChapterRef) (Chapter, error)
|
|
}
|
|
|
|
// RankingProvider can enumerate novels from a ranking page.
|
|
type RankingProvider interface {
|
|
// ScrapeRanking pages through up to maxPages ranking pages, sending BookMeta
|
|
// values (with basic info like title, cover, genres, status, sourceURL) to
|
|
// the returned channel. Pages are fetched sequentially and lazily: the next
|
|
// page is only requested once all entries from the current page have been
|
|
// sent. maxPages <= 0 means "all pages".
|
|
ScrapeRanking(ctx context.Context, maxPages int) (<-chan BookMeta, <-chan error)
|
|
}
|
|
|
|
// RankingPageCacher persists and retrieves raw HTML for individual ranking pages.
|
|
// Implementations (e.g. writer.Writer) store files on disk so that a
|
|
// subsequent ScrapeRanking call can serve cached HTML without a network round-trip.
|
|
type RankingPageCacher interface {
|
|
// WriteRankingPageCache stores the raw HTML string for the given page number.
|
|
WriteRankingPageCache(page int, html string) error
|
|
// ReadRankingPageCache returns the cached HTML for page, or ("", nil) on a miss.
|
|
ReadRankingPageCache(page int) (string, error)
|
|
}
|
|
|
|
// NovelScraper is the full interface that a concrete novel source must implement.
|
|
// It composes all four provider interfaces.
|
|
type NovelScraper interface {
|
|
CatalogueProvider
|
|
MetadataProvider
|
|
ChapterListProvider
|
|
ChapterTextProvider
|
|
RankingProvider
|
|
|
|
// SourceName returns the human-readable name of this scraper, e.g. "novelfire.net".
|
|
SourceName() string
|
|
}
|