diff --git a/backend/internal/novelfire/scraper.go b/backend/internal/novelfire/scraper.go
index 8b53944..f9e360f 100644
--- a/backend/internal/novelfire/scraper.go
+++ b/backend/internal/novelfire/scraper.go
@@ -178,12 +178,26 @@ func (s *Scraper) ScrapeMetadata(ctx context.Context, bookURL string) (domain.Bo
}
}
- status := htmlutil.ExtractFirst(root, scraper.Selector{Tag: "span", Class: "status"})
+ // Status: novelfire renders Ongoing (or
+ // "completed", "hiatus") inside the .header-stats block. We take the text
+ // content and lowercase it so the index value is always canonical lowercase.
+ var status string
+ for _, cls := range []string{"ongoing", "completed", "hiatus"} {
+ if v := htmlutil.ExtractFirst(root, scraper.Selector{Tag: "strong", Class: cls}); v != "" {
+ status = strings.ToLower(strings.TrimSpace(v))
+ break
+ }
+ }
- genresNode := htmlutil.FindFirst(root, scraper.Selector{Tag: "div", Class: "genres"})
+ // Genres: novelfire renders
- Genre
+ // Each is one genre tag. Lowercase for index consistency.
var genres []string
- if genresNode != nil {
- genres = htmlutil.ExtractAll(genresNode, scraper.Selector{Tag: "a", Multiple: true})
+ if categoriesNode := htmlutil.FindFirst(root, scraper.Selector{Tag: "div", Class: "categories"}); categoriesNode != nil {
+ for _, v := range htmlutil.ExtractAll(categoriesNode, scraper.Selector{Tag: "a", Class: "property-item", Multiple: true}) {
+ if v != "" {
+ genres = append(genres, strings.ToLower(strings.TrimSpace(v)))
+ }
+ }
}
summary := htmlutil.ExtractFirst(root, scraper.Selector{Tag: "div", Class: "summary"})
diff --git a/backend/internal/novelfire/scraper_test.go b/backend/internal/novelfire/scraper_test.go
index 04a9f7f..6c29a3b 100644
--- a/backend/internal/novelfire/scraper_test.go
+++ b/backend/internal/novelfire/scraper_test.go
@@ -2,6 +2,7 @@ package novelfire
import (
"context"
+ "log/slog"
"testing"
)
@@ -100,6 +101,56 @@ func TestRetryGet_EventualSuccess(t *testing.T) {
}
}
+// TestParseMetadataSelectors verifies that the status and genres selectors
+// match the current novelfire.net HTML structure.
+func TestParseMetadataSelectors(t *testing.T) {
+ // Minimal HTML reproducing the relevant novelfire.net book page structure.
+ const html = `
+
+
Shadow Slave
+Guiltythree
+
+
+
+123 Chapters
+`
+
+ stub := newStubClient()
+ stub.setFn("https://novelfire.net/book/shadow-slave", func() (string, error) {
+ return html, nil
+ })
+
+ s := &Scraper{client: stub, log: slog.Default()}
+ meta, err := s.ScrapeMetadata(t.Context(), "https://novelfire.net/book/shadow-slave")
+ if err != nil {
+ t.Fatalf("ScrapeMetadata: %v", err)
+ }
+
+ if meta.Status != "ongoing" {
+ t.Errorf("status = %q, want %q", meta.Status, "ongoing")
+ }
+
+ wantGenres := []string{"fantasy", "action", "adventure"}
+ if len(meta.Genres) != len(wantGenres) {
+ t.Fatalf("genres = %v, want %v", meta.Genres, wantGenres)
+ }
+ for i, g := range meta.Genres {
+ if g != wantGenres[i] {
+ t.Errorf("genres[%d] = %q, want %q", i, g, wantGenres[i])
+ }
+ }
+}
+
// ── minimal stub client for tests ─────────────────────────────────────────────
type stubClient struct {