The platform had three duplicated, divergent book resolvers (koreader, kobo, BookMatchingService) and none of them consulted media_item_formats.file_sha256 - per-format hashes for converted files (KEPUB, PDF) are computed and stored at import/conversion time but were never used for lookup. GetMediaItemFormatBySHA256 existed with zero callers. Any client holding a converted file could never match by hash. Add internal/services/book_resolver.go: a single shared resolution path from client-supplied identifier to media_item. ResolveBySHA256 checks media_items.file_sha256 first (indexed GetMediaItemBySHA256), then falls back to media_item_formats. file_sha256 (indexed GetMediaItemFormatBySHA256, first caller) so a converted format matches with equal confidence. The import-time SHA-256 is the canonical identifier shared by every interface. Wire two of the existing resolvers through it: - BookMatchingService.matchBySHA256: replaces the in-memory ListMediaItems scan of up to 1000 rows with the resolver's indexed lookups, and gains format awareness for the link/auto-link UI. MatchMethod now reports sha256_sha256 or sha256_sha256_format - KoboHandler.mapContentIdToBookhoardUUID: the SHA-256 heuristic branch (ContentId that looks like a 64-char hash) now resolves format-aware too. Kobo's entitlement_id wire identity is untouched; only the opportunistic hash branch changed
69 lines
2.6 KiB
Go
69 lines
2.6 KiB
Go
package services
|
|
|
|
import (
|
|
"bookhoard/internal/database"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
// ResolveMethod describes how a media item was resolved from a client-supplied identifier.
|
|
type ResolveMethod string
|
|
|
|
const (
|
|
MethodNone ResolveMethod = ""
|
|
MethodSHA256 ResolveMethod = "sha256" // matched on media_items.file_sha256
|
|
MethodSHA256Format ResolveMethod = "sha256_format" // matched on media_item_formats.file_sha256 (converted/alternate format)
|
|
)
|
|
|
|
// BookResolver is the single shared path from a client-supplied identifier to a
|
|
// media_item.
|
|
//
|
|
// All client/sync interfaces (koreader, kobo, OPDS, the device-link UI, and any
|
|
// future mobile app) should resolve books through BookResolver so they share
|
|
// identical matching semantics. In particular it provides format-aware SHA-256
|
|
// matching: a converted file (KEPUB/PDF) whose hash lives in media_item_formats
|
|
// resolves just as well as the primary format. The import-time SHA-256 is the
|
|
// canonical shared identifier across every client.
|
|
type BookResolver struct {
|
|
db *database.Queries
|
|
}
|
|
|
|
// NewBookResolver constructs a resolver backed by the given queries.
|
|
func NewBookResolver(db *database.Queries) *BookResolver {
|
|
return &BookResolver{db: db}
|
|
}
|
|
|
|
// ResolveBySHA256 resolves a media item by its content hash. It checks the
|
|
// primary media_items.file_sha256 first, then media_item_formats.file_sha256 so
|
|
// that a converted/alternate format (KEPUB, PDF, ...) also matches. Returns the
|
|
// matched item and how it matched, or pgx.ErrNoRows when no item has this hash.
|
|
func (r *BookResolver) ResolveBySHA256(ctx context.Context, sha256 string) (database.MediaItems, ResolveMethod, error) {
|
|
if sha256 == "" {
|
|
return database.MediaItems{}, MethodNone, pgx.ErrNoRows
|
|
}
|
|
sha := pgtype.Text{String: sha256, Valid: true}
|
|
|
|
// 1. Primary content hash (the file the media item was imported from).
|
|
if mi, err := r.db.GetMediaItemBySHA256(ctx, sha); err == nil {
|
|
return mi, MethodSHA256, nil
|
|
} else if !errors.Is(err, pgx.ErrNoRows) {
|
|
return database.MediaItems{}, MethodNone, fmt.Errorf("resolve by sha256 (primary): %w", err)
|
|
}
|
|
|
|
// 2. Per-format hash (a converted/alternate format: KEPUB, PDF, ...).
|
|
formatRow, err := r.db.GetMediaItemFormatBySHA256(ctx, sha)
|
|
if err == nil {
|
|
if mi, err := r.db.GetMediaItem(ctx, formatRow.MediaItemID); err == nil {
|
|
return mi, MethodSHA256Format, nil
|
|
}
|
|
} else if !errors.Is(err, pgx.ErrNoRows) {
|
|
return database.MediaItems{}, MethodNone, fmt.Errorf("resolve by sha256 (format): %w", err)
|
|
}
|
|
|
|
return database.MediaItems{}, MethodNone, pgx.ErrNoRows
|
|
}
|