Adds a scraping_tasks PocketBase collection with fields for kind, status, progress counters, timestamps, and error info. Exposes CreateScrapeTask, UpdateScrapeTask, and ListScrapeTasks on the Store interface with implementations in HybridStore and PocketBaseStore.
149 lines
7.8 KiB
Go
149 lines
7.8 KiB
Go
// Package storage defines the unified Store interface and helper types used by
|
|
// the server and orchestrator. Concrete implementations back the interface
|
|
// with PocketBase (structured data) and MinIO (binary objects).
|
|
package storage
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/libnovel/scraper/internal/scraper"
|
|
)
|
|
|
|
// ─── Shared types ─────────────────────────────────────────────────────────────
|
|
|
|
// ChapterInfo is a lightweight chapter descriptor (mirrors writer.ChapterInfo).
|
|
type ChapterInfo struct {
|
|
Number int
|
|
Title string
|
|
Date string
|
|
}
|
|
|
|
// RankingItem represents a single entry in the novel ranking list.
|
|
// Aliased from scraper.RankingItem for convenience within this package.
|
|
type RankingItem = scraper.RankingItem
|
|
|
|
// ReadingProgress holds a single user's reading position for one book.
|
|
type ReadingProgress struct {
|
|
Slug string `json:"slug"`
|
|
Chapter int `json:"chapter"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// ScrapeTask represents a single scraping job record from the scraping_tasks
|
|
// collection.
|
|
type ScrapeTask struct {
|
|
ID string `json:"id"`
|
|
Kind string `json:"kind"` // "catalogue" | "book"
|
|
TargetURL string `json:"target_url"` // non-empty for single-book scrapes
|
|
Status string `json:"status"` // "running" | "done" | "failed" | "cancelled"
|
|
BooksFound int `json:"books_found"`
|
|
ChaptersScraped int `json:"chapters_scraped"`
|
|
ChaptersSkipped int `json:"chapters_skipped"`
|
|
Errors int `json:"errors"`
|
|
Started time.Time `json:"started"`
|
|
Finished time.Time `json:"finished,omitempty"`
|
|
ErrorMessage string `json:"error_message,omitempty"`
|
|
}
|
|
|
|
// ScrapeTaskUpdate carries the fields that can be patched on a ScrapeTask.
|
|
// Zero-value fields are still sent; callers should only include keys they want
|
|
// to change via the map form used inside the store implementation.
|
|
type ScrapeTaskUpdate struct {
|
|
Status string
|
|
BooksFound int
|
|
ChaptersScraped int
|
|
ChaptersSkipped int
|
|
Errors int
|
|
Finished time.Time // zero = not finished yet
|
|
ErrorMessage string
|
|
}
|
|
|
|
// ─── Store interface ──────────────────────────────────────────────────────────
|
|
|
|
// Store is the single persistence abstraction consumed by the server and the
|
|
// orchestrator. Implementations may route calls to different backends
|
|
// (PocketBase for structured records, MinIO for binary blobs).
|
|
type Store interface {
|
|
// ── Book metadata ──────────────────────────────────────────────────────
|
|
|
|
// WriteMetadata upserts book metadata.
|
|
WriteMetadata(ctx context.Context, meta scraper.BookMeta) error
|
|
// ReadMetadata returns the metadata for slug. Returns (zero, false, nil)
|
|
// when the book is not found.
|
|
ReadMetadata(ctx context.Context, slug string) (scraper.BookMeta, bool, error)
|
|
// ListBooks returns all books, sorted alphabetically by title.
|
|
ListBooks(ctx context.Context) ([]scraper.BookMeta, error)
|
|
// LocalSlugs returns the set of slugs that have metadata stored.
|
|
LocalSlugs(ctx context.Context) (map[string]bool, error)
|
|
// MetadataMtime returns the Unix-second mtime of the metadata record, or 0.
|
|
MetadataMtime(ctx context.Context, slug string) int64
|
|
|
|
// ── Chapters (binary blobs in MinIO) ───────────────────────────────────
|
|
|
|
// ChapterExists returns true if the markdown file for the given ref exists.
|
|
ChapterExists(ctx context.Context, slug string, ref scraper.ChapterRef) bool
|
|
// WriteChapter stores the chapter markdown.
|
|
WriteChapter(ctx context.Context, slug string, chapter scraper.Chapter) error
|
|
// ReadChapter returns the raw markdown for chapter number n.
|
|
ReadChapter(ctx context.Context, slug string, n int) (string, error)
|
|
// ListChapters returns all stored chapters for slug, sorted by number.
|
|
ListChapters(ctx context.Context, slug string) ([]ChapterInfo, error)
|
|
// CountChapters returns the number of stored chapters for slug.
|
|
CountChapters(ctx context.Context, slug string) int
|
|
|
|
// ── Ranking ────────────────────────────────────────────────────────────
|
|
|
|
// 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)
|
|
// 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 ────────────────────────────────────────────────────────
|
|
|
|
// GetAudioCache returns the Kokoro filename for cacheKey, or ("", false).
|
|
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 ───────────────────────────────────────────────────
|
|
|
|
// GetProgress returns the reading progress for the given session ID and slug.
|
|
// Returns (zero, false) if no progress is recorded.
|
|
GetProgress(ctx context.Context, sessionID, slug string) (ReadingProgress, bool)
|
|
// SetProgress saves or updates reading progress.
|
|
SetProgress(ctx context.Context, sessionID string, p ReadingProgress) error
|
|
// AllProgress returns all progress entries for a session.
|
|
AllProgress(ctx context.Context, sessionID string) ([]ReadingProgress, error)
|
|
// DeleteProgress removes progress for a specific slug.
|
|
DeleteProgress(ctx context.Context, sessionID, slug string) error
|
|
|
|
// ── Audio object paths (MinIO) ─────────────────────────────────────────
|
|
|
|
// AudioObjectKey returns the MinIO object key for a cached audio file.
|
|
AudioObjectKey(slug string, n int, voice string, speed float64) string
|
|
|
|
// ── Presigned URLs ─────────────────────────────────────────────────────
|
|
|
|
// PresignChapter returns a presigned GET URL for a chapter markdown object.
|
|
PresignChapter(ctx context.Context, slug string, n int, expires time.Duration) (string, error)
|
|
|
|
// PresignAudio returns a presigned GET URL for an audio object.
|
|
PresignAudio(ctx context.Context, key string, expires time.Duration) (string, error)
|
|
|
|
// ── Scraping tasks ─────────────────────────────────────────────────────
|
|
|
|
// CreateScrapeTask inserts a new scraping_tasks record with status="running"
|
|
// and returns the assigned ID.
|
|
CreateScrapeTask(ctx context.Context, kind, targetURL string) (string, error)
|
|
// UpdateScrapeTask patches an existing task record.
|
|
UpdateScrapeTask(ctx context.Context, id string, u ScrapeTaskUpdate) error
|
|
// ListScrapeTasks returns all tasks sorted by started descending.
|
|
ListScrapeTasks(ctx context.Context) ([]ScrapeTask, error)
|
|
}
|