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

@@ -6,7 +6,6 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"sort"
"strconv"
"strings"
@@ -130,43 +129,23 @@ func (h *HybridStore) CountChapters(ctx context.Context, slug string) int {
// ─── Ranking ─────────────────────────────────────────────────────────────────
func (h *HybridStore) WriteRanking(ctx context.Context, items []RankingItem) error {
data, err := json.Marshal(items)
if err != nil {
return fmt.Errorf("storage: marshal ranking: %w", err)
}
return h.pb.SetRanking(ctx, string(data))
func (h *HybridStore) WriteRankingItem(ctx context.Context, item RankingItem) error {
return h.pb.UpsertRankingItem(ctx, item)
}
func (h *HybridStore) ReadRankingItems(ctx context.Context) ([]RankingItem, error) {
dataStr, _, err := h.pb.GetRanking(ctx)
if err != nil || dataStr == "" {
return nil, err
return h.pb.ListRankingItems(ctx)
}
func (h *HybridStore) RankingFreshEnough(ctx context.Context, maxAge time.Duration) (bool, error) {
last, err := h.pb.RankingLastUpdated(ctx)
if err != nil {
return false, err
}
var items []RankingItem
if err := json.Unmarshal([]byte(dataStr), &items); err != nil {
return nil, fmt.Errorf("storage: unmarshal ranking: %w", err)
if last.IsZero() {
return false, nil
}
return items, nil
}
func (h *HybridStore) RankingFileInfo(ctx context.Context) (os.FileInfo, error) {
return h.pb.RankingModTime(ctx)
}
// ─── Ranking page HTML cache ──────────────────────────────────────────────────
func (h *HybridStore) WriteRankingPageCache(ctx context.Context, page int, html string) error {
return h.pb.SetRankingPageHTML(ctx, page, html)
}
func (h *HybridStore) ReadRankingPageCache(ctx context.Context, page int) (string, error) {
html, _, err := h.pb.GetRankingPageHTML(ctx, page)
return html, err
}
func (h *HybridStore) RankingPageCacheInfo(ctx context.Context, page int) (os.FileInfo, error) {
return h.pb.RankingPageCacheModTime(ctx, page)
return time.Since(last) < maxAge, nil
}
// ─── Audio cache ──────────────────────────────────────────────────────────────
@@ -222,6 +201,12 @@ func (h *HybridStore) AudioObjectKey(slug string, n int, voice string, speed flo
return AudioObjectKey(slug, n, voice, speed)
}
// ─── PutAudio ─────────────────────────────────────────────────────────────────
func (h *HybridStore) PutAudio(ctx context.Context, key string, data []byte) error {
return h.minio.PutAudio(ctx, key, data)
}
// ─── Presigned URLs ───────────────────────────────────────────────────────────
func (h *HybridStore) PresignChapter(ctx context.Context, slug string, n int, expires time.Duration) (string, error) {
@@ -300,8 +285,11 @@ func splitChapterTitle(raw string) (title, date string) {
start = 0
}
numPart := strings.TrimSpace(raw[start:idx])
if _, err := strconv.Atoi(strings.Fields(numPart)[0]); err == nil {
return strings.TrimSpace(raw[:start]), strings.TrimSpace(raw[start : idx+len(suffix)])
fields := strings.Fields(numPart)
if len(fields) > 0 {
if _, err := strconv.Atoi(fields[0]); err == nil {
return strings.TrimSpace(raw[:start]), strings.TrimSpace(raw[start : idx+len(suffix)])
}
}
}
}