Per-annotation percentage derivation (deriveAnnotationPercentage) built a fresh CFIConverter for every highlight/note/bookmark, re-reading and re-parsing the whole EPUB each time. Export SectionPercentageCached so handlers reach the same bounded cache ConvertToCanonical already uses (8 books, insertion-order eviction): one parse per book per push instead of one per annotation.
173 lines
5.5 KiB
Go
173 lines
5.5 KiB
Go
package sync
|
|
|
|
import (
|
|
"log"
|
|
"sync"
|
|
)
|
|
|
|
type LocatorSource string
|
|
|
|
const (
|
|
LocatorSourceKOReader LocatorSource = "koreader"
|
|
LocatorSourceKobo LocatorSource = "kobo"
|
|
LocatorSourceWeb LocatorSource = "web"
|
|
)
|
|
|
|
type CanonicalLocator struct {
|
|
CFI string
|
|
Precision string
|
|
Percentage float64
|
|
}
|
|
|
|
type DeviceLocator struct {
|
|
Position string
|
|
Precision string
|
|
Percentage float64
|
|
}
|
|
|
|
func isConvertible(formatGroup string) bool {
|
|
return formatGroup == string(FormatGroupReflowable)
|
|
}
|
|
|
|
// Converters parse and cache the whole EPUB (spine + content docs), so
|
|
// creating one per annotation re-reads the book for every entry. A small
|
|
// bounded cache lets one request — or several — share a single parse.
|
|
// Servers are the right place for this work: clients stay thin.
|
|
var (
|
|
converterMu sync.Mutex
|
|
converterCache = map[string]*CFIConverter{}
|
|
converterOrder []string // insertion order for eviction
|
|
)
|
|
|
|
const maxCachedConverters = 8
|
|
|
|
func cachedConverter(epubPath string) *CFIConverter {
|
|
converterMu.Lock()
|
|
defer converterMu.Unlock()
|
|
if c, ok := converterCache[epubPath]; ok {
|
|
return c
|
|
}
|
|
c := NewCFIConverter(epubPath)
|
|
converterCache[epubPath] = c
|
|
converterOrder = append(converterOrder, epubPath)
|
|
for len(converterOrder) > maxCachedConverters {
|
|
oldest := converterOrder[0]
|
|
converterOrder = converterOrder[1:]
|
|
delete(converterCache, oldest)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// SectionPercentageCached resolves the spine-section percentage of a CRE
|
|
// xpointer through the shared bounded converter cache, so per-annotation
|
|
// lookups parse the EPUB once per book instead of once per annotation.
|
|
func SectionPercentageCached(epubPath, xpointer string) float64 {
|
|
return cachedConverter(epubPath).SectionPercentage(xpointer)
|
|
}
|
|
|
|
func ConvertToCanonical(
|
|
source LocatorSource,
|
|
devicePos string,
|
|
percentage float64,
|
|
contextText string,
|
|
formatGroup string,
|
|
epubPath string,
|
|
kepubPath string,
|
|
) CanonicalLocator {
|
|
if !isConvertible(formatGroup) || epubPath == "" {
|
|
return CanonicalLocator{
|
|
CFI: devicePos,
|
|
Precision: "passthrough",
|
|
Percentage: percentage,
|
|
}
|
|
}
|
|
|
|
switch source {
|
|
case LocatorSourceKOReader:
|
|
if !IsCREXPointer(devicePos) {
|
|
return CanonicalLocator{CFI: devicePos, Precision: "already-standard", Percentage: percentage}
|
|
}
|
|
converter := cachedConverter(epubPath)
|
|
result, err := converter.ConvertCREToStandard(devicePos, percentage, contextText)
|
|
if err != nil || result == nil {
|
|
log.Printf("Bookhoard: locator CRE→CFI conversion failed: %v", err)
|
|
return CanonicalLocator{CFI: devicePos, Precision: "fallback", Percentage: percentage}
|
|
}
|
|
if result.EPUBCFI != "" {
|
|
return CanonicalLocator{CFI: result.EPUBCFI, Precision: result.Precision, Percentage: result.Percentage}
|
|
}
|
|
if result.Href != "" {
|
|
return CanonicalLocator{CFI: result.Href, Precision: result.Precision, Percentage: result.Percentage}
|
|
}
|
|
return CanonicalLocator{CFI: devicePos, Precision: result.Precision, Percentage: percentage}
|
|
|
|
case LocatorSourceKobo:
|
|
if kepubPath == "" {
|
|
return CanonicalLocator{CFI: devicePos, Precision: "no-kepub", Percentage: percentage}
|
|
}
|
|
converter := NewKEPUBCFIConverter(epubPath, kepubPath)
|
|
result, err := converter.ConvertKEPUBCFIToStandard(devicePos, percentage, contextText)
|
|
if err != nil || result == nil {
|
|
log.Printf("Bookhoard: locator KEPUB→CFI conversion failed: %v", err)
|
|
return CanonicalLocator{CFI: devicePos, Precision: "fallback", Percentage: percentage}
|
|
}
|
|
if result.CFI != "" {
|
|
return CanonicalLocator{CFI: result.CFI, Precision: result.Precision, Percentage: result.Percentage}
|
|
}
|
|
return CanonicalLocator{CFI: devicePos, Precision: result.Precision, Percentage: percentage}
|
|
|
|
default:
|
|
return CanonicalLocator{CFI: devicePos, Precision: "passthrough", Percentage: percentage}
|
|
}
|
|
}
|
|
|
|
func ConvertFromCanonical(
|
|
source LocatorSource,
|
|
canonicalCFI string,
|
|
percentage float64,
|
|
contextText string,
|
|
formatGroup string,
|
|
epubPath string,
|
|
kepubPath string,
|
|
) DeviceLocator {
|
|
if !isConvertible(formatGroup) || epubPath == "" || canonicalCFI == "" {
|
|
return DeviceLocator{
|
|
Position: canonicalCFI,
|
|
Precision: "passthrough",
|
|
Percentage: percentage,
|
|
}
|
|
}
|
|
|
|
switch source {
|
|
case LocatorSourceKOReader:
|
|
converter := cachedConverter(epubPath)
|
|
result, err := converter.ConvertStandardToCRE(canonicalCFI, percentage, contextText)
|
|
if err != nil || result == nil {
|
|
log.Printf("Bookhoard: locator CFI→CRE conversion failed: %v", err)
|
|
return DeviceLocator{Position: canonicalCFI, Precision: "fallback", Percentage: percentage}
|
|
}
|
|
if result.XPointer != "" {
|
|
return DeviceLocator{Position: result.XPointer, Precision: result.Precision, Percentage: result.Percentage}
|
|
}
|
|
return DeviceLocator{Position: canonicalCFI, Precision: result.Precision, Percentage: percentage}
|
|
|
|
case LocatorSourceKobo:
|
|
if kepubPath == "" {
|
|
return DeviceLocator{Position: canonicalCFI, Precision: "no-kepub", Percentage: percentage}
|
|
}
|
|
converter := NewKEPUBCFIConverter(epubPath, kepubPath)
|
|
result, err := converter.ConvertStandardCFIToKEPUB(canonicalCFI, percentage, contextText)
|
|
if err != nil || result == nil {
|
|
log.Printf("Bookhoard: locator CFI→KEPUB conversion failed: %v", err)
|
|
return DeviceLocator{Position: canonicalCFI, Precision: "fallback", Percentage: percentage}
|
|
}
|
|
if result.CFI != "" {
|
|
return DeviceLocator{Position: result.CFI, Precision: result.Precision, Percentage: result.Percentage}
|
|
}
|
|
return DeviceLocator{Position: canonicalCFI, Precision: result.Precision, Percentage: percentage}
|
|
|
|
default:
|
|
return DeviceLocator{Position: canonicalCFI, Precision: "passthrough", Percentage: percentage}
|
|
}
|
|
}
|