Files

104 lines
3.8 KiB
Go
Raw Permalink Normal View History

package api
import (
"context"
"log/slog"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/Silo-Server/silo-server/internal/api/handlers"
"github.com/Silo-Server/silo-server/internal/catalog"
"github.com/Silo-Server/silo-server/internal/config"
"github.com/Silo-Server/silo-server/internal/ebookconvert"
)
// ebookKindleConversionSettingKey is the admin flag that gates Kindle->EPUB
// conversion. Off by default; toggled via PUT /admin/settings/{key}.
const ebookKindleConversionSettingKey = "ebook.kindle_conversion_enabled"
// buildEbookConversion wires the in-process Kindle->EPUB converter for the read
// handler. It compiles the embedded WASM module once at startup; if that fails
// the feature stays off (returns nil) and the raw original is served as before.
// The admin flag is read with a short TTL (see ebookFlagPredicate), so it can be
// toggled without a restart yet does not hit the DB on every read.
//
// The Converter (a wazero runtime, ~no external resources — per-conversion
// scratch dirs are cleaned as they go) is intentionally owned for the process
// lifetime: it is not Close()d on shutdown because process exit reclaims it and
// there is nothing to flush. Memory is bounded per conversion by
// ebookconvert.DefaultMaxMemoryPages × DefaultConcurrency (see the design doc's
// resource notes); tune via ebookconvert.Options if the deployment is tight.
func buildEbookConversion(deps Dependencies, settings catalog.SettingsStore) *handlers.EbookConversion {
if settings == nil {
return nil
}
converter, err := ebookconvert.NewConverter(context.Background(), ebookconvert.Options{})
if err != nil {
slog.Warn("ebook Kindle->EPUB conversion unavailable: converter init failed", "error", err)
return nil
}
cache, err := ebookconvert.NewCache(converter, ebookconvert.CacheOptions{Dir: ebookConversionCacheDir(deps.CurrentConfig())})
if err != nil {
slog.Warn("ebook Kindle->EPUB conversion unavailable: cache init failed", "error", err)
_ = converter.Close(context.Background())
return nil
}
slog.Info("ebook Kindle->EPUB conversion ready (admin-flag gated)", "setting", ebookKindleConversionSettingKey)
return &handlers.EbookConversion{
Converter: cache,
Enabled: ebookFlagPredicate(settings, ebookKindleConversionSettingKey),
}
}
// ebookFlagPredicate returns a cached boolean reader for a server setting. The
// read path (every ebook read + the capability endpoint, which clients may
// poll) would otherwise issue a DB query per request; a short TTL collapses
// bursts to one query while keeping admin toggles effectively instant. On a read
// error it keeps the last known value rather than flapping the feature off.
func ebookFlagPredicate(settings catalog.SettingsStore, key string) func(context.Context) bool {
const ttl = 3 * time.Second
var (
mu sync.Mutex
value bool
fetched time.Time
hasVal bool
)
return func(ctx context.Context) bool {
mu.Lock()
defer mu.Unlock()
if hasVal && time.Since(fetched) < ttl {
return value
}
v, err := settings.Get(ctx, key)
if err != nil {
return value // keep last known (false until the first successful read)
}
value = isTruthySetting(v)
fetched = time.Now()
hasVal = true
return value
}
}
// ebookConversionCacheDir derives the converted-EPUB cache directory, a sibling
// of the transcode dir so it lives alongside other derived media.
func ebookConversionCacheDir(cfg *config.Config) string {
base := os.TempDir()
if cfg != nil && strings.TrimSpace(cfg.Playback.TranscodeDir) != "" {
base = filepath.Dir(cfg.Playback.TranscodeDir)
}
return filepath.Join(base, "silo-ebook-epub")
}
func isTruthySetting(v string) bool {
switch strings.ToLower(strings.TrimSpace(v)) {
case "true", "1", "yes", "on", "enabled":
return true
default:
return false
}
}