diff --git a/internal/database/querier.go b/internal/database/querier.go index 248f6bd..e902dd1 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -23,6 +23,7 @@ type Querier interface { // Bulk update format group for all media items BulkUpdateFormatGroups(ctx context.Context) error BulkUpdateProgressFromSync(ctx context.Context, arg BulkUpdateProgressFromSyncParams) ([]interface{}, error) + CheckForProgressConflicts(ctx context.Context, arg CheckForProgressConflictsParams) (int64, error) // Cleanup expired OPDS tokens CleanupExpiredOpdsTokens(ctx context.Context) error CleanupExpiredRefreshTokens(ctx context.Context) error @@ -275,7 +276,9 @@ type Querier interface { IsBookOnKoboShelf(ctx context.Context, arg IsBookOnKoboShelfParams) (bool, error) // Link unlinked book to media item LinkUnlinkedBook(ctx context.Context, arg LinkUnlinkedBookParams) (UnlinkedBooks, error) + ListAllConflictsByUserAndStatus(ctx context.Context, arg ListAllConflictsByUserAndStatusParams) ([]ListAllConflictsByUserAndStatusRow, error) ListAllSyncQueueItems(ctx context.Context, arg ListAllSyncQueueItemsParams) ([]ListAllSyncQueueItemsRow, error) + ListConflictsByUser(ctx context.Context, userID pgtype.UUID) ([]ListConflictsByUserRow, error) ListDevicesByType(ctx context.Context, deviceType string) ([]Devices, error) ListDevicesByUser(ctx context.Context, userID pgtype.UUID) ([]Devices, error) ListLibraries(ctx context.Context) ([]ListLibrariesRow, error) diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index 3467be9..5274924 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -153,6 +153,28 @@ func (q *Queries) BulkUpdateProgressFromSync(ctx context.Context, arg BulkUpdate return items, nil } +const CheckForProgressConflicts = `-- name: CheckForProgressConflicts :one +SELECT COUNT(*) as conflict_count +FROM reading_progress +WHERE media_item_id = $1 + AND user_id = $2 + AND last_sync_timestamp > NOW() - INTERVAL '5 minutes' + AND last_sync_source != $3 +` + +type CheckForProgressConflictsParams struct { + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + LastSyncSource pgtype.Text `db:"last_sync_source" json:"last_sync_source"` +} + +func (q *Queries) CheckForProgressConflicts(ctx context.Context, arg CheckForProgressConflictsParams) (int64, error) { + row := q.db.QueryRow(ctx, CheckForProgressConflicts, arg.MediaItemID, arg.UserID, arg.LastSyncSource) + var conflict_count int64 + err := row.Scan(&conflict_count) + return conflict_count, err +} + const CleanupExpiredOpdsTokens = `-- name: CleanupExpiredOpdsTokens :exec DELETE FROM opds_tokens WHERE expires_at < NOW() ` @@ -6322,6 +6344,67 @@ func (q *Queries) LinkUnlinkedBook(ctx context.Context, arg LinkUnlinkedBookPara return i, err } +const ListAllConflictsByUserAndStatus = `-- name: ListAllConflictsByUserAndStatus :many +SELECT sc.id, sc.media_item_id, sc.user_id, sc.conflict_type, sc.conflict_data, sc.resolution_status, sc.resolution_data, sc.resolved_by, sc.resolved_at, sc.created_at, mi.title, mi.author +FROM sync_conflicts sc +JOIN media_items mi ON sc.media_item_id = mi.id +WHERE sc.user_id = $1 AND sc.resolution_status = $2 +ORDER BY sc.created_at DESC +` + +type ListAllConflictsByUserAndStatusParams struct { + UserID pgtype.UUID `db:"user_id" json:"user_id"` + ResolutionStatus pgtype.Text `db:"resolution_status" json:"resolution_status"` +} + +type ListAllConflictsByUserAndStatusRow struct { + ID pgtype.UUID `db:"id" json:"id"` + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + ConflictType string `db:"conflict_type" json:"conflict_type"` + ConflictData []byte `db:"conflict_data" json:"conflict_data"` + ResolutionStatus pgtype.Text `db:"resolution_status" json:"resolution_status"` + ResolutionData []byte `db:"resolution_data" json:"resolution_data"` + ResolvedBy pgtype.UUID `db:"resolved_by" json:"resolved_by"` + ResolvedAt pgtype.Timestamptz `db:"resolved_at" json:"resolved_at"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + Title string `db:"title" json:"title"` + Author pgtype.Text `db:"author" json:"author"` +} + +func (q *Queries) ListAllConflictsByUserAndStatus(ctx context.Context, arg ListAllConflictsByUserAndStatusParams) ([]ListAllConflictsByUserAndStatusRow, error) { + rows, err := q.db.Query(ctx, ListAllConflictsByUserAndStatus, arg.UserID, arg.ResolutionStatus) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListAllConflictsByUserAndStatusRow{} + for rows.Next() { + var i ListAllConflictsByUserAndStatusRow + if err := rows.Scan( + &i.ID, + &i.MediaItemID, + &i.UserID, + &i.ConflictType, + &i.ConflictData, + &i.ResolutionStatus, + &i.ResolutionData, + &i.ResolvedBy, + &i.ResolvedAt, + &i.CreatedAt, + &i.Title, + &i.Author, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const ListAllSyncQueueItems = `-- name: ListAllSyncQueueItems :many SELECT sq.id, sq.device_id, sq.media_item_id, sq.sync_type, sq.sync_data, sq.priority, sq.attempts, sq.max_attempts, sq.status, sq.error_message, sq.created_at, sq.processed_at, @@ -6398,6 +6481,62 @@ func (q *Queries) ListAllSyncQueueItems(ctx context.Context, arg ListAllSyncQueu return items, nil } +const ListConflictsByUser = `-- name: ListConflictsByUser :many +SELECT sc.id, sc.media_item_id, sc.user_id, sc.conflict_type, sc.conflict_data, sc.resolution_status, sc.resolution_data, sc.resolved_by, sc.resolved_at, sc.created_at, mi.title, mi.author +FROM sync_conflicts sc +JOIN media_items mi ON sc.media_item_id = mi.id +WHERE sc.user_id = $1 +ORDER BY sc.created_at DESC +` + +type ListConflictsByUserRow struct { + ID pgtype.UUID `db:"id" json:"id"` + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + ConflictType string `db:"conflict_type" json:"conflict_type"` + ConflictData []byte `db:"conflict_data" json:"conflict_data"` + ResolutionStatus pgtype.Text `db:"resolution_status" json:"resolution_status"` + ResolutionData []byte `db:"resolution_data" json:"resolution_data"` + ResolvedBy pgtype.UUID `db:"resolved_by" json:"resolved_by"` + ResolvedAt pgtype.Timestamptz `db:"resolved_at" json:"resolved_at"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + Title string `db:"title" json:"title"` + Author pgtype.Text `db:"author" json:"author"` +} + +func (q *Queries) ListConflictsByUser(ctx context.Context, userID pgtype.UUID) ([]ListConflictsByUserRow, error) { + rows, err := q.db.Query(ctx, ListConflictsByUser, userID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListConflictsByUserRow{} + for rows.Next() { + var i ListConflictsByUserRow + if err := rows.Scan( + &i.ID, + &i.MediaItemID, + &i.UserID, + &i.ConflictType, + &i.ConflictData, + &i.ResolutionStatus, + &i.ResolutionData, + &i.ResolvedBy, + &i.ResolvedAt, + &i.CreatedAt, + &i.Title, + &i.Author, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const ListDevicesByType = `-- name: ListDevicesByType :many SELECT id, user_id, device_name, device_type, device_identifier, auth_token, last_sync, last_seen, sync_enabled, auto_sync, sync_frequency_minutes, device_metadata, created_at, updated_at FROM devices WHERE device_type = $1 ORDER BY created_at DESC ` diff --git a/internal/database/queries/queries.sql b/internal/database/queries/queries.sql index 70bdfbb..f1b8131 100644 --- a/internal/database/queries/queries.sql +++ b/internal/database/queries/queries.sql @@ -1099,19 +1099,19 @@ RETURNING *; -- name: DeleteSyncConflict :exec DELETE FROM sync_conflicts WHERE id = $1; - -- name: ListAllConflictsByUserAndStatus :many - SELECT sc.*, mi.title, mi.author - FROM sync_conflicts sc - JOIN media_items mi ON sc.media_item_id = mi.id - WHERE sc.user_id = $1 AND sc.resolution_status = $2 - ORDER BY sc.created_at DESC; +-- name: ListAllConflictsByUserAndStatus :many +SELECT sc.*, mi.title, mi.author +FROM sync_conflicts sc +JOIN media_items mi ON sc.media_item_id = mi.id +WHERE sc.user_id = $1 AND sc.resolution_status = $2 +ORDER BY sc.created_at DESC; - -- name: ListConflictsByUser :many - SELECT sc.*, mi.title, mi.author - FROM sync_conflicts sc - JOIN media_items mi ON sc.media_item_id = mi.id - WHERE sc.user_id = $1 - ORDER BY sc.created_at DESC; +-- name: ListConflictsByUser :many +SELECT sc.*, mi.title, mi.author +FROM sync_conflicts sc +JOIN media_items mi ON sc.media_item_id = mi.id +WHERE sc.user_id = $1 +ORDER BY sc.created_at DESC; -- ============================================ -- KOREADER SYNC PROTOCOL @@ -1216,7 +1216,7 @@ WHERE lv.user_id = $1 ORDER BY mi.title ASC LIMIT 1000; - -- name: CheckForProgressConflicts :one +-- name: CheckForProgressConflicts :one SELECT COUNT(*) as conflict_count FROM reading_progress WHERE media_item_id = $1 diff --git a/internal/handlers/conflicts.go b/internal/handlers/conflicts.go index 82f253b..a2a2210 100644 --- a/internal/handlers/conflicts.go +++ b/internal/handlers/conflicts.go @@ -401,7 +401,7 @@ func (h *ConflictHandler) DeleteConflict(c *echo.Context) error { func (h *ConflictHandler) DismissAllResolved(c *echo.Context) error { user := c.Get("user").(database.Users) - conflicts, err := h.db.ListSyncConflictsByUser(context.Background(), user.ID) + conflicts, err := h.db.ListConflictsByUser(context.Background(), user.ID) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "failed to list conflicts") }