- Speed is no longer part of Kokoro generation, in-memory cache keys, or MinIO object keys — audio is always generated at 1.0 and playback speed is applied client-side via audioEl.playbackRate - presignAudio() now calls rewriteHost() so chapter audio URLs use the public MinIO endpoint (same as presignVoiceSample already did) - docker-compose.yml: rename MINIO_PUBLIC_ENDPOINT → PUBLIC_MINIO_PUBLIC_URL for the ui service so SvelteKit's $env/dynamic/public picks it up
173 lines
9.2 KiB
Go
173 lines
9.2 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
|
|
// ReindexChapters rebuilds chapters_idx from MinIO objects for slug.
|
|
// Returns the number of chapters indexed.
|
|
ReindexChapters(ctx context.Context, slug string) (int, error)
|
|
|
|
// ── 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) string
|
|
// AudioExists returns true when the audio object is present in the bucket.
|
|
AudioExists(ctx context.Context, key string) bool
|
|
|
|
// ── 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)
|
|
|
|
// ── Browse page snapshots (MinIO) ──────────────────────────────────────
|
|
|
|
// SaveBrowsePage stores a SingleFile HTML snapshot for the given cache key.
|
|
SaveBrowsePage(ctx context.Context, key, html string) error
|
|
// GetBrowsePage retrieves a cached HTML snapshot. Returns ("", false, nil)
|
|
// when no snapshot exists for the key.
|
|
GetBrowsePage(ctx context.Context, key string) (string, bool, error)
|
|
// BrowseHTMLKey returns the MinIO object key for a SingleFile HTML snapshot.
|
|
// Layout: {domain}/html/page-{n}.html
|
|
BrowseHTMLKey(domain string, page int) string
|
|
// BrowseCoverKey returns the MinIO object key for a cached book cover image.
|
|
// Layout: {domain}/assets/book-covers/{slug}.jpg
|
|
BrowseCoverKey(domain, slug string) string
|
|
// SaveBrowseAsset stores a binary asset (e.g. a cover image) in the browse bucket.
|
|
SaveBrowseAsset(ctx context.Context, key string, data []byte, contentType string) error
|
|
// GetBrowseAsset retrieves a binary asset from the browse bucket.
|
|
// Returns (nil, "", false, nil) when the object does not exist.
|
|
GetBrowseAsset(ctx context.Context, key string) ([]byte, string, bool, 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)
|
|
}
|