feat(koreader): device bridge for ratings, reviews and reading-time stats
Release / build-and-push (push) Successful in 3m4s
Release / build-and-push (push) Successful in 3m4s
Extends the device-authed koreader surface with the handoff item 4 Batch B contract: - GET/POST /api/sync/koreader/rating — the device mirrors the .sdr summary's 5-star rating against the 1-10 server scale (POST 0 clears). - GET/POST /api/sync/koreader/review — raw-markdown review mirror (summary.note); empty body clears, matching the first-party PUT. - POST /api/sync/koreader/stats — the device pushes its CUMULATIVE total reading seconds; the server watermark (highest total ever converted, read from the koreader sessions' own device_metadata) converts only the delta into a reading_history session attributed to the koreader device. Reinstalls cannot double-count; sub-minute deltas ride forward (no session, no watermark movement); a lagging device clamps to zero instead of dragging the total back. Book identity resolves through the existing priority chain (uuid → sha256 → alias → title/author). GET binding uses query tags; POST endpoints bind the ref+payload once (an earlier resolve-then-bind draft drained the body). Live-verified against the dev server with the koreader device token: rating set/get/clear, review mirror, and the stats sequence 900→session, 960→delta session, 970→rides forward, 500→clamps; the book's stats card and the By Device panel both pick up the split.
This commit is contained in:
1 parent
2117ec6181
commit
2517baedea
5 files changed
+308
No files matched your search
@@ -235,6 +235,12 @@ type Querier interface {
|
||||
GetKoboShelfBooksByShelfName(ctx context.Context, arg GetKoboShelfBooksByShelfNameParams) ([]GetKoboShelfBooksByShelfNameRow, error)
|
||||
// Get Kobo shelves by collection
|
||||
GetKoboShelvesByCollection(ctx context.Context, arg GetKoboShelvesByCollectionParams) ([]GetKoboShelvesByCollectionRow, error)
|
||||
// KOReader stats conversion: the device pushes its CUMULATIVE total reading
|
||||
// seconds for a book; the watermark is the highest total already converted,
|
||||
// read from the koreader sessions' own device_metadata. Delta-only insertion
|
||||
// means a reinstall of the device cannot double-count, and sub-threshold
|
||||
// deltas are never lost (they stay folded into the next push's delta).
|
||||
GetKoreaderStatWatermark(ctx context.Context, arg GetKoreaderStatWatermarkParams) (interface{}, error)
|
||||
GetLibrary(ctx context.Context, id pgtype.UUID) (GetLibraryRow, error)
|
||||
GetLibraryByFolder(ctx context.Context, folderPath string) (GetLibraryByFolderRow, error)
|
||||
GetLibraryByFolderPathPrefix(ctx context.Context, folderPath string) (GetLibraryByFolderPathPrefixRow, error)
|
||||
|
||||
@@ -4624,6 +4624,31 @@ func (q *Queries) GetKoboShelvesByCollection(ctx context.Context, arg GetKoboShe
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetKoreaderStatWatermark = `-- name: GetKoreaderStatWatermark :one
|
||||
SELECT COALESCE(MAX((device_metadata->>'total_seconds')::bigint), 0) AS total_seconds
|
||||
FROM reading_history
|
||||
WHERE user_id = $1
|
||||
AND media_item_id = $2
|
||||
AND device_metadata->>'source' = 'koreader'
|
||||
`
|
||||
|
||||
type GetKoreaderStatWatermarkParams struct {
|
||||
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
|
||||
}
|
||||
|
||||
// KOReader stats conversion: the device pushes its CUMULATIVE total reading
|
||||
// seconds for a book; the watermark is the highest total already converted,
|
||||
// read from the koreader sessions' own device_metadata. Delta-only insertion
|
||||
// means a reinstall of the device cannot double-count, and sub-threshold
|
||||
// deltas are never lost (they stay folded into the next push's delta).
|
||||
func (q *Queries) GetKoreaderStatWatermark(ctx context.Context, arg GetKoreaderStatWatermarkParams) (interface{}, error) {
|
||||
row := q.db.QueryRow(ctx, GetKoreaderStatWatermark, arg.UserID, arg.MediaItemID)
|
||||
var total_seconds interface{}
|
||||
err := row.Scan(&total_seconds)
|
||||
return total_seconds, err
|
||||
}
|
||||
|
||||
const GetLibrary = `-- name: GetLibrary :one
|
||||
SELECT l.id, l.name, l.description, l.library_type_id, l.created_by_admin_id, l.created_at, l.updated_at, lt.name as type_name, lt.description as type_description
|
||||
FROM libraries l
|
||||
|
||||
@@ -565,6 +565,18 @@ SELECT * FROM media_reviews WHERE media_item_id = $1 AND user_id = $2;
|
||||
-- name: DeleteMediaReview :exec
|
||||
DELETE FROM media_reviews WHERE media_item_id = $1 AND user_id = $2;
|
||||
|
||||
-- KOReader stats conversion: the device pushes its CUMULATIVE total reading
|
||||
-- seconds for a book; the watermark is the highest total already converted,
|
||||
-- read from the koreader sessions' own device_metadata. Delta-only insertion
|
||||
-- means a reinstall of the device cannot double-count, and sub-threshold
|
||||
-- deltas are never lost (they stay folded into the next push's delta).
|
||||
-- name: GetKoreaderStatWatermark :one
|
||||
SELECT COALESCE(MAX((device_metadata->>'total_seconds')::bigint), 0) AS total_seconds
|
||||
FROM reading_history
|
||||
WHERE user_id = $1
|
||||
AND media_item_id = $2
|
||||
AND device_metadata->>'source' = 'koreader';
|
||||
|
||||
-- Search Media Items queries
|
||||
-- name: SearchMediaItems :many
|
||||
SELECT mi.*, l.name as library_name, lt.name as library_type_name
|
||||
|
||||
Reference in new issue
Block a user