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 }