All checks were successful
Release / Test backend (push) Successful in 48s
Release / Check ui (push) Successful in 55s
Release / Docker / caddy (push) Successful in 38s
Release / Docker / backend (push) Successful in 3m21s
Release / Docker / runner (push) Successful in 2m41s
Release / Upload source maps (push) Successful in 3m44s
Release / Docker / ui (push) Successful in 2m42s
Release / Gitea Release (push) Successful in 1m22s
- New `ai_jobs` PocketBase collection tracks all long-running AI tasks (batch-covers, chapter-names) with status, progress, and cancellation - `handlers_aijobs.go`: GET/cancel endpoints for ai_jobs; centralised cancel registry (moved from handlers_catalogue) - Batch-covers and chapter-names SSE handlers now create/resume ai_job records, support from_item/to_item ranges, and resume from items_done on restart via job_id - New `POST /api/admin/image-gen/auto-prompt`: generates an image prompt from book description (cover) or chapter title (chapter) via LLM - image-gen page: "Auto-prompt" button calls auto-prompt API when a slug is selected; falls back gracefully if TextGen not configured - text-gen chapter-names: from/to chapter range inputs + job ID display - catalogue-tools batch-covers: from/to item range + resume job ID input - pb-init-v3.sh: adds ai_jobs collection (idempotent) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
189 lines
8.4 KiB
Go
189 lines
8.4 KiB
Go
// Package bookstore defines the segregated read/write interfaces for book,
|
|
// chapter, ranking, progress, audio, and presign data.
|
|
//
|
|
// Interface segregation:
|
|
// - BookWriter — used by the runner to persist scraped data.
|
|
// - BookReader — used by the backend to serve book/chapter data.
|
|
// - RankingStore — used by both runner (write) and backend (read).
|
|
// - PresignStore — used only by the backend for URL signing.
|
|
// - AudioStore — used by the runner to store audio; backend for presign.
|
|
// - ProgressStore— used only by the backend for reading progress.
|
|
//
|
|
// Concrete implementations live in internal/storage.
|
|
package bookstore
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"time"
|
|
|
|
"github.com/libnovel/backend/internal/domain"
|
|
)
|
|
|
|
// BookWriter is the write side used by the runner after scraping a book.
|
|
type BookWriter interface {
|
|
// WriteMetadata upserts all bibliographic fields for a book.
|
|
WriteMetadata(ctx context.Context, meta domain.BookMeta) error
|
|
|
|
// WriteChapter stores a fully-scraped chapter's text in MinIO and
|
|
// updates the chapters_idx record in PocketBase.
|
|
WriteChapter(ctx context.Context, slug string, chapter domain.Chapter) error
|
|
|
|
// WriteChapterRefs persists chapter metadata (number + title) into
|
|
// chapters_idx without fetching or storing chapter text.
|
|
WriteChapterRefs(ctx context.Context, slug string, refs []domain.ChapterRef) error
|
|
|
|
// ChapterExists returns true if the markdown object for ref already exists.
|
|
ChapterExists(ctx context.Context, slug string, ref domain.ChapterRef) bool
|
|
|
|
// DeduplicateChapters removes duplicate chapters_idx records for slug,
|
|
// keeping only one record per chapter number (the one with the latest
|
|
// updated timestamp). Returns the number of duplicate records deleted.
|
|
DeduplicateChapters(ctx context.Context, slug string) (int, error)
|
|
}
|
|
|
|
// BookReader is the read side used by the backend to serve content.
|
|
type BookReader interface {
|
|
// ReadMetadata returns the metadata for slug.
|
|
// Returns (zero, false, nil) when not found.
|
|
ReadMetadata(ctx context.Context, slug string) (domain.BookMeta, bool, error)
|
|
|
|
// ListBooks returns all books sorted alphabetically by title.
|
|
ListBooks(ctx context.Context) ([]domain.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
|
|
|
|
// 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) ([]domain.ChapterInfo, error)
|
|
|
|
// CountChapters returns the count of stored chapters.
|
|
CountChapters(ctx context.Context, slug string) int
|
|
|
|
// ReindexChapters rebuilds chapters_idx from MinIO objects for slug.
|
|
ReindexChapters(ctx context.Context, slug string) (int, error)
|
|
}
|
|
|
|
// RankingStore covers ranking reads and writes.
|
|
type RankingStore interface {
|
|
// WriteRankingItem upserts a single ranking entry (keyed on Slug).
|
|
WriteRankingItem(ctx context.Context, item domain.RankingItem) error
|
|
|
|
// ReadRankingItems returns all ranking items sorted by rank ascending.
|
|
ReadRankingItems(ctx context.Context) ([]domain.RankingItem, error)
|
|
|
|
// RankingFreshEnough returns true when ranking rows exist and the most
|
|
// recent Updated timestamp is within maxAge.
|
|
RankingFreshEnough(ctx context.Context, maxAge time.Duration) (bool, error)
|
|
}
|
|
|
|
// AudioStore covers audio object storage (runner writes; backend reads).
|
|
type AudioStore interface {
|
|
// AudioObjectKey returns the MinIO object key for a cached MP3 audio file.
|
|
// Format: {slug}/{n}/{voice}.mp3
|
|
AudioObjectKey(slug string, n int, voice string) string
|
|
|
|
// AudioObjectKeyExt returns the MinIO object key for a cached audio file
|
|
// with a custom extension (e.g. "mp3" or "wav").
|
|
AudioObjectKeyExt(slug string, n int, voice, ext string) string
|
|
|
|
// AudioExists returns true when the audio object is present in MinIO.
|
|
AudioExists(ctx context.Context, key string) bool
|
|
|
|
// PutAudio stores raw audio bytes under the given MinIO object key.
|
|
PutAudio(ctx context.Context, key string, data []byte) error
|
|
|
|
// PutAudioStream uploads audio from r to MinIO under key.
|
|
// size must be the exact byte length of r, or -1 to use multipart upload.
|
|
// contentType should be "audio/mpeg" or "audio/wav".
|
|
PutAudioStream(ctx context.Context, key string, r io.Reader, size int64, contentType string) error
|
|
}
|
|
|
|
// PresignStore generates short-lived URLs — used exclusively by the backend.
|
|
type PresignStore interface {
|
|
// 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)
|
|
|
|
// PresignAvatarUpload returns a short-lived presigned PUT URL for uploading
|
|
// an avatar image. ext should be "jpg", "png", or "webp".
|
|
PresignAvatarUpload(ctx context.Context, userID, ext string) (uploadURL, key string, err error)
|
|
|
|
// PresignAvatarURL returns a presigned GET URL for a user's avatar.
|
|
// Returns ("", false, nil) when no avatar exists.
|
|
PresignAvatarURL(ctx context.Context, userID string) (string, bool, error)
|
|
|
|
// PutAvatar stores raw image bytes for a user avatar directly in MinIO.
|
|
// ext should be "jpg", "png", or "webp". Returns the object key.
|
|
PutAvatar(ctx context.Context, userID, ext, contentType string, data []byte) (key string, err error)
|
|
|
|
// DeleteAvatar removes all avatar objects for a user.
|
|
DeleteAvatar(ctx context.Context, userID string) error
|
|
}
|
|
|
|
// ProgressStore covers per-session reading progress — backend only.
|
|
type ProgressStore interface {
|
|
// GetProgress returns the reading progress for the given session + slug.
|
|
GetProgress(ctx context.Context, sessionID, slug string) (domain.ReadingProgress, bool)
|
|
|
|
// SetProgress saves or updates reading progress.
|
|
SetProgress(ctx context.Context, sessionID string, p domain.ReadingProgress) error
|
|
|
|
// AllProgress returns all progress entries for a session.
|
|
AllProgress(ctx context.Context, sessionID string) ([]domain.ReadingProgress, error)
|
|
|
|
// DeleteProgress removes progress for a specific slug.
|
|
DeleteProgress(ctx context.Context, sessionID, slug string) error
|
|
}
|
|
|
|
// CoverStore covers book cover image storage in MinIO.
|
|
// The runner writes covers during catalogue refresh; the backend reads them.
|
|
type CoverStore interface {
|
|
// PutCover stores a raw cover image for a book identified by slug.
|
|
PutCover(ctx context.Context, slug string, data []byte, contentType string) error
|
|
|
|
// GetCover retrieves the cover image for a book. Returns (nil, false, nil)
|
|
// when no cover exists for the given slug.
|
|
GetCover(ctx context.Context, slug string) ([]byte, string, bool, error)
|
|
|
|
// CoverExists returns true when a cover image is stored for slug.
|
|
CoverExists(ctx context.Context, slug string) bool
|
|
}
|
|
|
|
// AIJobStore manages AI generation jobs tracked in PocketBase.
|
|
type AIJobStore interface {
|
|
// CreateAIJob inserts a new ai_job record with status=running and returns its ID.
|
|
CreateAIJob(ctx context.Context, job domain.AIJob) (string, error)
|
|
// GetAIJob retrieves a single ai_job by ID.
|
|
// Returns (zero, false, nil) when not found.
|
|
GetAIJob(ctx context.Context, id string) (domain.AIJob, bool, error)
|
|
// UpdateAIJob patches an existing ai_job record with the given fields.
|
|
UpdateAIJob(ctx context.Context, id string, fields map[string]any) error
|
|
// ListAIJobs returns all ai_job records sorted by started descending.
|
|
ListAIJobs(ctx context.Context) ([]domain.AIJob, error)
|
|
}
|
|
|
|
// TranslationStore covers machine-translated chapter storage in MinIO.
|
|
// The runner writes translations; the backend reads them.
|
|
type TranslationStore interface {
|
|
// TranslationObjectKey returns the MinIO object key for a cached translation.
|
|
TranslationObjectKey(lang, slug string, n int) string
|
|
|
|
// TranslationExists returns true when the translation object is present in MinIO.
|
|
TranslationExists(ctx context.Context, key string) bool
|
|
|
|
// PutTranslation stores raw translated markdown under the given MinIO object key.
|
|
PutTranslation(ctx context.Context, key string, data []byte) error
|
|
|
|
// GetTranslation retrieves translated markdown from MinIO.
|
|
GetTranslation(ctx context.Context, key string) (string, error)
|
|
}
|