refactor(ranking): replace blob cache with per-item PocketBase storage

- Replace SetRanking/GetRanking/SetRankingPageHTML/GetRankingPageHTML blob methods
  with WriteRankingItem/ReadRankingItems/RankingFreshEnough per-item operations
- Add 24h staleness gate in ScrapeRanking to skip re-scraping fresh data
- Add GET /api/ranking endpoint returning []RankingItem sorted by rank
- Remove RankingPageCacher interface and rankingCacheAdapter adapter
- Update integration tests to use new per-item upsert semantics
- Include e2e test suite (scraper/internal/e2e/)
This commit is contained in:
Admin
2026-03-03 19:37:49 +05:00
parent 56bf4dde22
commit b8d4d94b18
10 changed files with 1173 additions and 351 deletions

View File

@@ -5,7 +5,6 @@ package storage
import (
"context"
"os"
"time"
"github.com/libnovel/scraper/internal/scraper"
@@ -22,14 +21,15 @@ type ChapterInfo struct {
// RankingItem represents a single entry in the novel ranking list.
type RankingItem struct {
Rank int `json:"rank"`
Slug string `json:"slug"`
Title string `json:"title"`
Author string `json:"author,omitempty"`
Cover string `json:"cover,omitempty"`
Status string `json:"status,omitempty"`
Genres []string `json:"genres,omitempty"`
SourceURL string `json:"source_url,omitempty"`
Rank int `json:"rank"`
Slug string `json:"slug"`
Title string `json:"title"`
Author string `json:"author,omitempty"`
Cover string `json:"cover,omitempty"`
Status string `json:"status,omitempty"`
Genres []string `json:"genres,omitempty"`
SourceURL string `json:"source_url,omitempty"`
Updated time.Time `json:"updated,omitempty"`
}
// ReadingProgress holds a single user's reading position for one book.
@@ -81,22 +81,13 @@ type Store interface {
// ── Ranking ────────────────────────────────────────────────────────────
// WriteRanking persists the ranking list.
WriteRanking(ctx context.Context, items []RankingItem) error
// ReadRankingItems returns the stored ranking items.
// WriteRankingItem upserts a single ranking entry (keyed on Slug).
WriteRankingItem(ctx context.Context, item RankingItem) error
// ReadRankingItems returns all ranking items sorted by rank ascending.
ReadRankingItems(ctx context.Context) ([]RankingItem, error)
// RankingFileInfo returns os.FileInfo-like data for the ranking record.
// Returns (nil, nil) when no ranking has been stored yet.
RankingFileInfo(ctx context.Context) (os.FileInfo, error)
// ── Ranking page HTML cache ────────────────────────────────────────────
// WriteRankingPageCache stores raw HTML for a ranking page.
WriteRankingPageCache(ctx context.Context, page int, html string) error
// ReadRankingPageCache returns cached HTML for a ranking page, or "" on miss.
ReadRankingPageCache(ctx context.Context, page int) (string, error)
// RankingPageCacheInfo returns file-like info for a cached ranking page.
RankingPageCacheInfo(ctx context.Context, page int) (os.FileInfo, error)
// RankingFreshEnough returns true when ranking rows exist and the most
// recent Updated timestamp is within maxAge of now.
RankingFreshEnough(ctx context.Context, maxAge time.Duration) (bool, error)
// ── Audio cache ────────────────────────────────────────────────────────
@@ -104,6 +95,8 @@ type Store interface {
GetAudioCache(ctx context.Context, cacheKey string) (string, bool)
// SetAudioCache persists a Kokoro filename for cacheKey.
SetAudioCache(ctx context.Context, cacheKey, filename string) error
// PutAudio stores raw audio bytes under the given MinIO object key.
PutAudio(ctx context.Context, key string, data []byte) error
// ── Reading progress ───────────────────────────────────────────────────