ConvertToCanonical/ConvertFromCanonical built a fresh CFIConverter per call, and each annotation converts twice (pos0+pos1) — a book with 200 highlights re-opened and re-parsed the EPUB 400+ times per sync, and again per metadata pull. A bounded 8-entry cache keyed by path now shares converters (the parsing work belongs on the server; clients stay thin). CFIConverter gained a mutex around its lazily built spine/doc caches since instances are now shared between concurrent requests. Adds CFIConverter.SectionPercentage: book-wide percentage for a CRE xpointer from the spine char distribution (midpoint of its document) — the server-side counterpart to dropping per-annotation getPageFromXPointer lookups from the plugin.
166 lines
5.2 KiB
Go
166 lines
5.2 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
|
|
}
|
|
|
|
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}
|
|
}
|
|
}
|