// 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" "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 } // 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 audio file. AudioObjectKey(slug string, n int, voice 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 } // 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) // 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 } // BrowseStore covers browse page snapshot storage. // The runner writes snapshots; the backend reads them. type BrowseStore interface { // PutBrowsePage stores a raw JSON snapshot for a browse page. // genre, sort, status, novelType and page identify the page. PutBrowsePage(ctx context.Context, genre, sort, status, novelType string, page int, data []byte) error // GetBrowsePage retrieves a raw JSON snapshot. Returns (nil, false, nil) // when no snapshot exists for the given parameters. GetBrowsePage(ctx context.Context, genre, sort, status, novelType string, page int) ([]byte, bool, error) }