feat(db): Add kobo_shelves and device catalog tables

- Add kobo_shelves table for Kobo device shelves management
- Add device_shelf_mappings for collection<->device shelf mappings
- Update device_catalogs with better ContentId tracking
- Add indexes for performance
This commit is contained in:
2026-01-31 22:32:04 -05:00
parent 60380a043d
commit 63008001c6
5 changed files with 2910 additions and 64 deletions
+167 -4
View File
@@ -735,8 +735,8 @@ BEGIN
-- If still not found, try to match by title + author
IF media_uuid IS NULL THEN
SELECT id INTO media_uuid FROM media_items
WHERE title = book_record->>'title'
SELECT id INTO media_uuid FROM media_items
WHERE title = book_record->>'title'
AND author = book_record->>'author'
LIMIT 1;
END IF;
@@ -751,7 +751,7 @@ BEGIN
conflict_detected := detect_conflict(media_uuid, book_record, 'koreader');
-- Check if progress exists
SELECT * INTO existing_progress FROM reading_progress
SELECT * INTO existing_progress FROM reading_progress
WHERE media_item_id = media_uuid AND user_id = p_user_id;
-- Update or insert progress
@@ -816,8 +816,171 @@ BEGIN
));
END IF;
RETURN QUERY SELECT media_uuid, TRUE,
RETURN QUERY SELECT media_uuid, TRUE,
CASE WHEN conflict_detected THEN 'conflict detected' ELSE 'success' END;
END LOOP;
END;
$$ LANGUAGE plpgsql;
-- ============================================
-- PHASE 1: UNIVERSAL BOOK IDENTIFIERS (Week 1)
-- ============================================
-- Add universal identifier columns to media_items table
ALTER TABLE media_items ADD COLUMN IF NOT EXISTS file_sha256 CHAR(64);
ALTER TABLE media_items ADD COLUMN IF NOT EXISTS opf_identifier VARCHAR(255);
ALTER TABLE media_items ADD COLUMN IF NOT EXISTS opf_uuid VARCHAR(255);
ALTER TABLE media_items ADD COLUMN IF NOT EXISTS hash_confidence VARCHAR(20);
-- Create indexes for fast lookup
CREATE INDEX IF NOT EXISTS idx_media_items_sha256 ON media_items(file_sha256);
CREATE INDEX IF NOT EXISTS idx_media_items_opf_identifier ON media_items(opf_identifier);
-- Create media_item_formats table (track all format versions with their hashes)
CREATE TABLE IF NOT EXISTS media_item_formats (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
media_item_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
format_type VARCHAR(10) NOT NULL,
file_path VARCHAR(500),
file_sha256 CHAR(64),
file_size_bytes BIGINT,
mime_type VARCHAR(100),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
converted_from_format_id UUID REFERENCES media_item_formats(id),
UNIQUE(media_item_id, format_type)
);
CREATE INDEX IF NOT EXISTS idx_media_item_formats_media ON media_item_formats(media_item_id, format_type);
CREATE INDEX IF NOT EXISTS idx_media_item_formats_sha256 ON media_item_formats(file_sha256);
-- Create device_file_aliases table (track file paths per device for cross-device matching)
CREATE TABLE IF NOT EXISTS device_file_aliases (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
media_item_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
device_id UUID NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
file_path VARCHAR(500) NOT NULL,
file_sha256 CHAR(64),
confidence_score FLOAT DEFAULT 1.0,
last_seen_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(device_id, file_path)
);
CREATE INDEX IF NOT EXISTS idx_device_file_aliases_media_device ON device_file_aliases(media_item_id, device_id);
CREATE INDEX IF NOT EXISTS idx_device_file_aliases_sha256 ON device_file_aliases(file_sha256);
-- Create collections table (device-neutral collections)
CREATE TABLE IF NOT EXISTS collections (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
description TEXT,
color VARCHAR(7),
icon VARCHAR(50),
auto_assign_rules JSONB,
view_settings JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(user_id, name)
);
CREATE INDEX IF NOT EXISTS idx_collections_user_id ON collections(user_id);
-- Create collection_items table (which books belong to each collection)
CREATE TABLE IF NOT EXISTS collection_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
collection_id UUID NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
media_item_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
added_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
added_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
UNIQUE(collection_id, media_item_id)
);
CREATE INDEX IF NOT EXISTS idx_collection_items_collection ON collection_items(collection_id);
CREATE INDEX IF NOT EXISTS idx_collection_items_media ON collection_items(media_item_id);
-- Create device_shelf_mappings table (map Bookmann collections to device-specific shelf names)
CREATE TABLE IF NOT EXISTS device_shelf_mappings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
collection_id UUID NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
device_id UUID NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
device_shelf_name VARCHAR(100),
sync_direction VARCHAR(20),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(collection_id, device_id)
);
CREATE INDEX IF NOT EXISTS idx_device_shelf_mappings_collection ON device_shelf_mappings(collection_id);
CREATE INDEX IF NOT EXISTS idx_device_shelf_mappings_device ON device_shelf_mappings(device_id);
-- Create device_catalogs table (track OPDS downloads and map Bookmann UUIDs to device ContentIds)
CREATE TABLE IF NOT EXISTS device_catalogs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
device_id UUID NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
media_item_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
bookmann_uuid UUID NOT NULL,
kobo_content_id VARCHAR(255) NOT NULL,
content_id_type VARCHAR(20),
available BOOLEAN DEFAULT TRUE,
delivery_date TIMESTAMP WITH TIME ZONE,
delivery_method VARCHAR(20),
UNIQUE(device_id, kobo_content_id)
);
CREATE INDEX IF NOT EXISTS idx_device_catalogs_bookmann ON device_catalogs(bookmann_uuid);
CREATE INDEX IF NOT EXISTS idx_device_catalogs_kobo ON device_catalogs(kobo_content_id);
-- Create system_config table (system-wide configuration)
CREATE TABLE IF NOT EXISTS system_config (
key VARCHAR(100) PRIMARY KEY,
value TEXT NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_by UUID REFERENCES users(id)
);
-- Pre-seeded values
INSERT INTO system_config (key, value) VALUES
('base_url', 'https://bookmann.example.com'),
('opds_base_url', 'https://bookmann.example.com/opds'),
('api_base_url', 'https://bookmann.example.com/api')
ON CONFLICT (key) DO NOTHING;
-- Create opds_tokens table (device-specific OPDS access tokens)
CREATE TABLE IF NOT EXISTS opds_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
device_id UUID NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
token VARCHAR(64) UNIQUE NOT NULL,
token_type VARCHAR(20),
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_opds_tokens_device ON opds_tokens(device_id);
CREATE INDEX IF NOT EXISTS idx_opds_tokens_token ON opds_tokens(token);
-- Modify kobo_shelves table (reference collections instead of media_items directly)
ALTER TABLE kobo_shelves ADD COLUMN IF NOT EXISTS collection_id UUID REFERENCES collections(id);
ALTER TABLE kobo_shelves ADD COLUMN IF NOT EXISTS position_in_collection INTEGER;
-- ============================================
-- PHASE 6: UNLINKED BOOKS TRACKING (Week 3-4)
-- ============================================
-- Create unlinked_books table to track books that couldn't be auto-matched
CREATE TABLE IF NOT EXISTS unlinked_books (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
device_id UUID NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
content_id VARCHAR(255) NOT NULL,
file_path VARCHAR(500),
title VARCHAR(255),
author VARCHAR(255),
confidence_score FLOAT DEFAULT 0.5,
resolved BOOLEAN DEFAULT FALSE,
media_item_id UUID REFERENCES media_items(id) ON DELETE SET NULL,
resolved_at TIMESTAMP WITH TIME ZONE,
resolution_method VARCHAR(50),
last_seen_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_unlinked_books_device ON unlinked_books(device_id);
CREATE INDEX IF NOT EXISTS idx_unlinked_books_content_id ON unlinked_books(content_id);
CREATE INDEX IF NOT EXISTS idx_unlinked_books_resolved ON unlinked_books(resolved);
+108 -7
View File
@@ -8,6 +8,57 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
type CollectionItems struct {
ID pgtype.UUID `db:"id" json:"id"`
CollectionID pgtype.UUID `db:"collection_id" json:"collection_id"`
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
AddedAt pgtype.Timestamptz `db:"added_at" json:"added_at"`
AddedByUserID pgtype.UUID `db:"added_by_user_id" json:"added_by_user_id"`
}
type Collections struct {
ID pgtype.UUID `db:"id" json:"id"`
UserID pgtype.UUID `db:"user_id" json:"user_id"`
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
Color pgtype.Text `db:"color" json:"color"`
Icon pgtype.Text `db:"icon" json:"icon"`
AutoAssignRules []byte `db:"auto_assign_rules" json:"auto_assign_rules"`
ViewSettings []byte `db:"view_settings" json:"view_settings"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
}
type DeviceCatalogs struct {
ID pgtype.UUID `db:"id" json:"id"`
DeviceID pgtype.UUID `db:"device_id" json:"device_id"`
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
BookmannUuid pgtype.UUID `db:"bookmann_uuid" json:"bookmann_uuid"`
KoboContentID string `db:"kobo_content_id" json:"kobo_content_id"`
ContentIDType pgtype.Text `db:"content_id_type" json:"content_id_type"`
Available pgtype.Bool `db:"available" json:"available"`
DeliveryDate pgtype.Timestamptz `db:"delivery_date" json:"delivery_date"`
DeliveryMethod pgtype.Text `db:"delivery_method" json:"delivery_method"`
}
type DeviceFileAliases struct {
ID pgtype.UUID `db:"id" json:"id"`
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
DeviceID pgtype.UUID `db:"device_id" json:"device_id"`
FilePath string `db:"file_path" json:"file_path"`
FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"`
ConfidenceScore pgtype.Float8 `db:"confidence_score" json:"confidence_score"`
LastSeenAt pgtype.Timestamptz `db:"last_seen_at" json:"last_seen_at"`
}
type DeviceShelfMappings struct {
ID pgtype.UUID `db:"id" json:"id"`
CollectionID pgtype.UUID `db:"collection_id" json:"collection_id"`
DeviceID pgtype.UUID `db:"device_id" json:"device_id"`
DeviceShelfName pgtype.Text `db:"device_shelf_name" json:"device_shelf_name"`
SyncDirection pgtype.Text `db:"sync_direction" json:"sync_direction"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
}
type Devices struct {
ID pgtype.UUID `db:"id" json:"id"`
UserID pgtype.UUID `db:"user_id" json:"user_id"`
@@ -163,13 +214,15 @@ type KoboEntitlements struct {
}
type KoboShelves struct {
ID pgtype.UUID `db:"id" json:"id"`
DeviceID pgtype.UUID `db:"device_id" json:"device_id"`
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
ShelfName pgtype.Text `db:"shelf_name" json:"shelf_name"`
ShelfPosition pgtype.Int4 `db:"shelf_position" json:"shelf_position"`
AddedAt pgtype.Timestamptz `db:"added_at" json:"added_at"`
LastSyncedAt pgtype.Timestamptz `db:"last_synced_at" json:"last_synced_at"`
ID pgtype.UUID `db:"id" json:"id"`
DeviceID pgtype.UUID `db:"device_id" json:"device_id"`
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
ShelfName pgtype.Text `db:"shelf_name" json:"shelf_name"`
ShelfPosition pgtype.Int4 `db:"shelf_position" json:"shelf_position"`
AddedAt pgtype.Timestamptz `db:"added_at" json:"added_at"`
LastSyncedAt pgtype.Timestamptz `db:"last_synced_at" json:"last_synced_at"`
CollectionID pgtype.UUID `db:"collection_id" json:"collection_id"`
PositionInCollection pgtype.Int4 `db:"position_in_collection" json:"position_in_collection"`
}
type Libraries struct {
@@ -230,6 +283,18 @@ type MediaHighlights struct {
DeviceSyncData []byte `db:"device_sync_data" json:"device_sync_data"`
}
type MediaItemFormats struct {
ID pgtype.UUID `db:"id" json:"id"`
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
FormatType string `db:"format_type" json:"format_type"`
FilePath pgtype.Text `db:"file_path" json:"file_path"`
FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"`
FileSizeBytes pgtype.Int8 `db:"file_size_bytes" json:"file_size_bytes"`
MimeType pgtype.Text `db:"mime_type" json:"mime_type"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
ConvertedFromFormatID pgtype.UUID `db:"converted_from_format_id" json:"converted_from_format_id"`
}
type MediaItems struct {
ID pgtype.UUID `db:"id" json:"id"`
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
@@ -277,6 +342,10 @@ type MediaItems struct {
RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"`
KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"`
KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"`
FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"`
OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"`
OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"`
HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"`
}
type MediaNotes struct {
@@ -306,6 +375,15 @@ type MediaRatings struct {
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
}
type OpdsTokens struct {
ID pgtype.UUID `db:"id" json:"id"`
DeviceID pgtype.UUID `db:"device_id" json:"device_id"`
Token string `db:"token" json:"token"`
TokenType pgtype.Text `db:"token_type" json:"token_type"`
ExpiresAt pgtype.Timestamptz `db:"expires_at" json:"expires_at"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
}
type ReadingHistory struct {
ID pgtype.UUID `db:"id" json:"id"`
UserID pgtype.UUID `db:"user_id" json:"user_id"`
@@ -383,6 +461,29 @@ type SyncQueue struct {
ProcessedAt pgtype.Timestamptz `db:"processed_at" json:"processed_at"`
}
type SystemConfig struct {
Key string `db:"key" json:"key"`
Value string `db:"value" json:"value"`
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
UpdatedBy pgtype.UUID `db:"updated_by" json:"updated_by"`
}
type UnlinkedBooks struct {
ID pgtype.UUID `db:"id" json:"id"`
DeviceID pgtype.UUID `db:"device_id" json:"device_id"`
ContentID string `db:"content_id" json:"content_id"`
FilePath pgtype.Text `db:"file_path" json:"file_path"`
Title pgtype.Text `db:"title" json:"title"`
Author pgtype.Text `db:"author" json:"author"`
ConfidenceScore pgtype.Float8 `db:"confidence_score" json:"confidence_score"`
Resolved pgtype.Bool `db:"resolved" json:"resolved"`
MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"`
ResolvedAt pgtype.Timestamptz `db:"resolved_at" json:"resolved_at"`
ResolutionMethod pgtype.Text `db:"resolution_method" json:"resolution_method"`
LastSeenAt pgtype.Timestamptz `db:"last_seen_at" json:"last_seen_at"`
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
}
type Users struct {
ID pgtype.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
+127
View File
@@ -11,6 +11,9 @@ import (
)
type Querier interface {
// COLLECTION ITEMS QUERIES
// Add book to collection
AddBookToCollection(ctx context.Context, arg AddBookToCollectionParams) (CollectionItems, error)
// ============================================
// KOBO SHELF MANAGEMENT QUERIES (Phase 4)
// ============================================
@@ -20,16 +23,32 @@ type Querier interface {
// Bulk update format group for all media items
BulkUpdateFormatGroups(ctx context.Context) error
BulkUpdateProgressFromSync(ctx context.Context, arg BulkUpdateProgressFromSyncParams) ([]interface{}, error)
// Cleanup expired OPDS tokens
CleanupExpiredOpdsTokens(ctx context.Context) error
CleanupExpiredRefreshTokens(ctx context.Context) error
ClearDeviceSyncQueue(ctx context.Context, deviceID pgtype.UUID) error
ClearKoboShelf(ctx context.Context, deviceID pgtype.UUID) error
ClearKoboShelfByName(ctx context.Context, arg ClearKoboShelfByNameParams) error
// Count unlinked books for a device
CountUnlinkedBooks(ctx context.Context, deviceID pgtype.UUID) (int64, error)
CountUserDevices(ctx context.Context, userID pgtype.UUID) (int64, error)
// COLLECTIONS QUERIES
// Create collection
CreateCollection(ctx context.Context, arg CreateCollectionParams) (Collections, error)
// ============================================
// PHASE 2: DEVICE MANAGEMENT & AUTH (Weeks 5-6)
// ============================================
// Device Registration & Management
CreateDevice(ctx context.Context, arg CreateDeviceParams) (Devices, error)
// DEVICE CATALOGS QUERIES
// Create device catalog entry
CreateDeviceCatalog(ctx context.Context, arg CreateDeviceCatalogParams) (DeviceCatalogs, error)
// DEVICE FILE ALIASES QUERIES
// Create device file alias
CreateDeviceFileAlias(ctx context.Context, arg CreateDeviceFileAliasParams) (DeviceFileAliases, error)
// DEVICE SHELF MAPPINGS QUERIES
// Create device shelf mapping
CreateDeviceShelfMapping(ctx context.Context, arg CreateDeviceShelfMappingParams) (DeviceShelfMappings, error)
// Backward compatibility - Ebook Notes queries (using views)
CreateEbookNote(ctx context.Context, arg CreateEbookNoteParams) (MediaNotes, error)
// Libraries queries
@@ -38,9 +57,15 @@ type Querier interface {
CreateMediaHighlight(ctx context.Context, arg CreateMediaHighlightParams) (MediaHighlights, error)
// Media Items queries
CreateMediaItem(ctx context.Context, arg CreateMediaItemParams) (MediaItems, error)
// MEDIA ITEM FORMATS QUERIES
// Create media item format
CreateMediaItemFormat(ctx context.Context, arg CreateMediaItemFormatParams) (MediaItemFormats, error)
// Media Notes queries
CreateMediaNote(ctx context.Context, arg CreateMediaNoteParams) (MediaNotes, error)
CreateMediaRating(ctx context.Context, arg CreateMediaRatingParams) (MediaRatings, error)
// OPDS TOKENS QUERIES
// Create OPDS token
CreateOpdsToken(ctx context.Context, arg CreateOpdsTokenParams) (OpdsTokens, error)
// ============================================
// KOBO ENTITLEMENT QUERIES (Phase 4)
// ============================================
@@ -54,26 +79,71 @@ type Querier interface {
CreateSyncHistoryEntry(ctx context.Context, arg CreateSyncHistoryEntryParams) (SyncQueue, error)
// Sync Queue Management
CreateSyncQueueItem(ctx context.Context, arg CreateSyncQueueItemParams) (SyncQueue, error)
// ============================================
// PHASE 6: ENHANCED KOBO SYNC (Week 3-4)
// ============================================
// Create unlinked book entry
CreateUnlinkedBook(ctx context.Context, arg CreateUnlinkedBookParams) (UnlinkedBooks, error)
CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error)
// Delete collection
DeleteCollection(ctx context.Context, id pgtype.UUID) error
DeleteDevice(ctx context.Context, id pgtype.UUID) error
DeleteDeviceByToken(ctx context.Context, authToken string) error
// Delete device catalog entry
DeleteDeviceCatalog(ctx context.Context, id pgtype.UUID) error
// Delete device file alias
DeleteDeviceFileAlias(ctx context.Context, id pgtype.UUID) error
// Delete device shelf mapping
DeleteDeviceShelfMapping(ctx context.Context, id pgtype.UUID) error
DeleteEbookNote(ctx context.Context, id pgtype.UUID) error
DeleteKoboEntitlement(ctx context.Context, arg DeleteKoboEntitlementParams) error
DeleteLibrary(ctx context.Context, id pgtype.UUID) error
DeleteLibraryFolder(ctx context.Context, arg DeleteLibraryFolderParams) (LibraryFolders, error)
DeleteMediaHighlight(ctx context.Context, id pgtype.UUID) error
DeleteMediaItem(ctx context.Context, id pgtype.UUID) error
// Delete media item format
DeleteMediaItemFormat(ctx context.Context, id pgtype.UUID) error
DeleteMediaNote(ctx context.Context, id pgtype.UUID) error
DeleteMediaRating(ctx context.Context, arg DeleteMediaRatingParams) error
DeleteReadingProgress(ctx context.Context, arg DeleteReadingProgressParams) error
DeleteSyncConflict(ctx context.Context, id pgtype.UUID) error
DeleteSyncQueueItem(ctx context.Context, id pgtype.UUID) error
// Delete system config
DeleteSystemConfig(ctx context.Context, key string) error
DeleteUser(ctx context.Context, id pgtype.UUID) error
GenerateKoboEntitlementId(ctx context.Context) (interface{}, error)
// Get all system config
GetAllSystemConfig(ctx context.Context) ([]SystemConfig, error)
GetAnnotationsForBook(ctx context.Context, arg GetAnnotationsForBookParams) ([]GetAnnotationsForBookRow, error)
// Get collection
GetCollection(ctx context.Context, id pgtype.UUID) (Collections, error)
// Get collection items
GetCollectionItems(ctx context.Context, collectionID pgtype.UUID) ([]GetCollectionItemsRow, error)
// Get collection with book count
GetCollectionWithBookCount(ctx context.Context, id pgtype.UUID) (GetCollectionWithBookCountRow, error)
// Get collections by user
GetCollectionsByUser(ctx context.Context, userID pgtype.UUID) ([]Collections, error)
// Get collections for book
GetCollectionsForBook(ctx context.Context, mediaItemID pgtype.UUID) ([]Collections, error)
GetDevice(ctx context.Context, id pgtype.UUID) (Devices, error)
GetDeviceByAuthToken(ctx context.Context, authToken string) (Devices, error)
GetDeviceByIdentifier(ctx context.Context, deviceIdentifier string) (Devices, error)
// Get device catalog by Bookmann UUID
GetDeviceCatalogByBookmannUUID(ctx context.Context, arg GetDeviceCatalogByBookmannUUIDParams) (DeviceCatalogs, error)
// Get device catalog by Kobo ContentId
GetDeviceCatalogByKoboContentId(ctx context.Context, koboContentID string) (DeviceCatalogs, error)
// Get device catalog entries
GetDeviceCatalogEntries(ctx context.Context, deviceID pgtype.UUID) ([]GetDeviceCatalogEntriesRow, error)
// Get device file alias
GetDeviceFileAlias(ctx context.Context, arg GetDeviceFileAliasParams) (DeviceFileAliases, error)
// Get device file alias by SHA-256
GetDeviceFileAliasBySHA256(ctx context.Context, fileSha256 pgtype.Text) (GetDeviceFileAliasBySHA256Row, error)
// Get device file aliases by device
GetDeviceFileAliasesByDevice(ctx context.Context, deviceID pgtype.UUID) ([]GetDeviceFileAliasesByDeviceRow, error)
// Get device shelf mapping
GetDeviceShelfMapping(ctx context.Context, arg GetDeviceShelfMappingParams) (DeviceShelfMappings, error)
// Get device shelf mappings
GetDeviceShelfMappings(ctx context.Context, deviceID pgtype.UUID) ([]GetDeviceShelfMappingsRow, error)
GetFailedSyncQueueItems(ctx context.Context, limit int32) ([]SyncQueue, error)
GetKoboEntitlementByContentId(ctx context.Context, arg GetKoboEntitlementByContentIdParams) (GetKoboEntitlementByContentIdRow, error)
GetKoboEntitlementByEntitlementId(ctx context.Context, arg GetKoboEntitlementByEntitlementIdParams) (GetKoboEntitlementByEntitlementIdRow, error)
@@ -81,6 +151,8 @@ type Querier interface {
GetKoboShelfBookCount(ctx context.Context, deviceID pgtype.UUID) (int64, error)
GetKoboShelfBooks(ctx context.Context, deviceID pgtype.UUID) ([]GetKoboShelfBooksRow, error)
GetKoboShelfBooksByShelfName(ctx context.Context, arg GetKoboShelfBooksByShelfNameParams) ([]GetKoboShelfBooksByShelfNameRow, error)
// Get Kobo shelves by collection
GetKoboShelvesByCollection(ctx context.Context, arg GetKoboShelvesByCollectionParams) ([]GetKoboShelvesByCollectionRow, error)
GetLibrary(ctx context.Context, id pgtype.UUID) (GetLibraryRow, error)
GetLibraryByFolder(ctx context.Context, folderPath string) (GetLibraryByFolderRow, error)
GetLibraryFolders(ctx context.Context, libraryID pgtype.UUID) ([]LibraryFolders, error)
@@ -98,11 +170,27 @@ type Querier interface {
// ============================================
GetMediaItemByFilePathForSync(ctx context.Context, filePath string) (MediaItems, error)
GetMediaItemByKoboContentId(ctx context.Context, koboContentID pgtype.Text) (MediaItems, error)
// Get media item by OPF identifier
GetMediaItemByOPFIdentifier(ctx context.Context, opfIdentifier pgtype.Text) (MediaItems, error)
// Get media item by OPF UUID
GetMediaItemByOPFUUID(ctx context.Context, opfUuid pgtype.Text) (MediaItems, error)
// Get media item by SHA-256 hash
GetMediaItemBySHA256(ctx context.Context, fileSha256 pgtype.Text) (MediaItems, error)
// Get media item format by SHA-256
GetMediaItemFormatBySHA256(ctx context.Context, fileSha256 pgtype.Text) (MediaItemFormats, error)
// Get media item format by type
GetMediaItemFormatByType(ctx context.Context, arg GetMediaItemFormatByTypeParams) (MediaItemFormats, error)
// Get media item formats
GetMediaItemFormats(ctx context.Context, mediaItemID pgtype.UUID) ([]MediaItemFormats, error)
GetMediaNote(ctx context.Context, id pgtype.UUID) (MediaNotes, error)
GetMediaNotes(ctx context.Context, arg GetMediaNotesParams) ([]MediaNotes, error)
GetMediaRating(ctx context.Context, arg GetMediaRatingParams) (MediaRatings, error)
GetMediaRatings(ctx context.Context, mediaItemID pgtype.UUID) ([]GetMediaRatingsRow, error)
GetNextRetryTime(ctx context.Context) (interface{}, error)
// Get OPDS token
GetOpdsToken(ctx context.Context, token string) (GetOpdsTokenRow, error)
// Get OPDS tokens by device
GetOpdsTokensByDevice(ctx context.Context, deviceID pgtype.UUID) ([]OpdsTokens, error)
// Get reading history for a user and book
GetReadingHistory(ctx context.Context, arg GetReadingHistoryParams) ([]ReadingHistory, error)
GetReadingProgress(ctx context.Context, arg GetReadingProgressParams) (ReadingProgress, error)
@@ -112,8 +200,15 @@ type Querier interface {
GetSyncConflict(ctx context.Context, id pgtype.UUID) (SyncConflicts, error)
GetSyncQueueItem(ctx context.Context, id pgtype.UUID) (SyncQueue, error)
GetSyncQueueStats(ctx context.Context, deviceID pgtype.UUID) (GetSyncQueueStatsRow, error)
// SYSTEM CONFIG QUERIES
// Get system config
GetSystemConfig(ctx context.Context, key string) (SystemConfig, error)
// Get universal progress for a book
GetUniversalProgress(ctx context.Context, arg GetUniversalProgressParams) (GetUniversalProgressRow, error)
// Get unlinked book by ContentId
GetUnlinkedBookByContentId(ctx context.Context, arg GetUnlinkedBookByContentIdParams) (UnlinkedBooks, error)
// Get unlinked books for a device
GetUnlinkedBooksByDevice(ctx context.Context, deviceID pgtype.UUID) ([]GetUnlinkedBooksByDeviceRow, error)
GetUser(ctx context.Context, id pgtype.UUID) (GetUserRow, error)
GetUserByEmail(ctx context.Context, email string) (GetUserByEmailRow, error)
GetUserByEmailOrUsername(ctx context.Context, email string) (GetUserByEmailOrUsernameRow, error)
@@ -124,7 +219,11 @@ type Querier interface {
GetUserProgressForBooks(ctx context.Context, arg GetUserProgressForBooksParams) ([]GetUserProgressForBooksRow, error)
GetUserVisibleLibraries(ctx context.Context, userID pgtype.UUID) ([]GetUserVisibleLibrariesRow, error)
IncrementSyncQueueAttempts(ctx context.Context, arg IncrementSyncQueueAttemptsParams) (SyncQueue, error)
// Check if book is in collection
IsBookInCollection(ctx context.Context, arg IsBookInCollectionParams) (bool, error)
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)
ListDevicesByType(ctx context.Context, deviceType string) ([]Devices, error)
@@ -138,10 +237,18 @@ type Querier interface {
ListSyncConflictsByMediaItem(ctx context.Context, arg ListSyncConflictsByMediaItemParams) ([]SyncConflicts, error)
ListSyncConflictsByUser(ctx context.Context, userID pgtype.UUID) ([]ListSyncConflictsByUserRow, error)
ListUsers(ctx context.Context) ([]ListUsersRow, error)
// Query media items by multiple identifiers with confidence scoring
QueryMediaItemsByIdentifiers(ctx context.Context, arg QueryMediaItemsByIdentifiersParams) ([]QueryMediaItemsByIdentifiersRow, error)
// Remove book from collection
RemoveBookFromCollection(ctx context.Context, arg RemoveBookFromCollectionParams) error
RemoveBookFromKoboShelf(ctx context.Context, arg RemoveBookFromKoboShelfParams) error
ResolveSyncConflict(ctx context.Context, arg ResolveSyncConflictParams) (SyncConflicts, error)
// Resolve unlinked book
ResolveUnlinkedBook(ctx context.Context, arg ResolveUnlinkedBookParams) (UnlinkedBooks, error)
RevokeAllUserRefreshTokens(ctx context.Context, userID pgtype.UUID) error
RevokeDevice(ctx context.Context, id pgtype.UUID) error
// Revoke OPDS token
RevokeOpdsToken(ctx context.Context, token string) error
RevokeRefreshToken(ctx context.Context, token pgtype.UUID) error
// Note: User ebook folders replaced by library folders system
// Legacy folder management is now handled through libraries
@@ -150,23 +257,43 @@ type Querier interface {
SearchMediaItemsFuzzy(ctx context.Context, arg SearchMediaItemsFuzzyParams) ([]SearchMediaItemsFuzzyRow, error)
// Library Visibility queries
SetLibraryVisibility(ctx context.Context, arg SetLibraryVisibilityParams) (LibraryVisibility, error)
// Set system config
SetSystemConfig(ctx context.Context, arg SetSystemConfigParams) (SystemConfig, error)
// Update collection
UpdateCollection(ctx context.Context, arg UpdateCollectionParams) (Collections, error)
UpdateDevice(ctx context.Context, arg UpdateDeviceParams) (Devices, error)
// Update device catalog availability
UpdateDeviceCatalogAvailability(ctx context.Context, arg UpdateDeviceCatalogAvailabilityParams) error
// Update device file alias
UpdateDeviceFileAlias(ctx context.Context, arg UpdateDeviceFileAliasParams) (DeviceFileAliases, error)
UpdateDeviceLastSeen(ctx context.Context, id pgtype.UUID) (Devices, error)
UpdateDeviceLastSync(ctx context.Context, id pgtype.UUID) (Devices, error)
// Update device shelf mapping
UpdateDeviceShelfMapping(ctx context.Context, arg UpdateDeviceShelfMappingParams) (DeviceShelfMappings, error)
UpdateDeviceSyncTimestamp(ctx context.Context, id pgtype.UUID) (Devices, error)
UpdateEbookNote(ctx context.Context, arg UpdateEbookNoteParams) (MediaNotes, error)
UpdateEmail(ctx context.Context, arg UpdateEmailParams) error
UpdateKoboEntitlementRevision(ctx context.Context, arg UpdateKoboEntitlementRevisionParams) error
UpdateKoboEntitlementStatus(ctx context.Context, arg UpdateKoboEntitlementStatusParams) error
UpdateKoboShelfBookPosition(ctx context.Context, arg UpdateKoboShelfBookPositionParams) error
// Update Kobo shelves to support collections
UpdateKoboShelfCollection(ctx context.Context, arg UpdateKoboShelfCollectionParams) (KoboShelves, error)
UpdateLibrary(ctx context.Context, arg UpdateLibraryParams) (Libraries, error)
UpdateMediaHighlight(ctx context.Context, arg UpdateMediaHighlightParams) (MediaHighlights, error)
UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams) (MediaItems, error)
// Update media item format
UpdateMediaItemFormat(ctx context.Context, arg UpdateMediaItemFormatParams) (MediaItemFormats, error)
// ============================================
// PHASE 1: FORMAT DETECTION & PROGRESS (Week 2)
// ============================================
// Update media item format group information
UpdateMediaItemFormatGroup(ctx context.Context, arg UpdateMediaItemFormatGroupParams) error
// Media Items Admin Operations
// ============================================
// PHASE 1: UNIVERSAL BOOK IDENTIFIERS (Week 1)
// ============================================
// Update media item with universal identifiers
UpdateMediaItemIdentifiers(ctx context.Context, arg UpdateMediaItemIdentifiersParams) (MediaItems, error)
UpdateMediaItemKoboMetadata(ctx context.Context, arg UpdateMediaItemKoboMetadataParams) (MediaItems, error)
UpdateMediaNote(ctx context.Context, arg UpdateMediaNoteParams) (MediaNotes, error)
UpdateMediaRating(ctx context.Context, arg UpdateMediaRatingParams) (MediaRatings, error)
File diff suppressed because it is too large Load Diff
+418 -5
View File
@@ -919,13 +919,14 @@ SELECT
mi.chapter_count,
mi.entitlement_id,
mi.revision_number,
mi.file_size
mi.file_size,
mi.file_sha256
FROM media_items mi
JOIN library_visibility lv ON mi.library_id = lv.library_id
WHERE lv.user_id = $1
WHERE lv.user_id = $1
AND lv.is_visible = true
ORDER BY mi.title ASC
LIMIT 1000;
ORDER BY mi.title ASC
LIMIT 1000;
-- name: CheckForProgressConflicts :one
SELECT COUNT(*) as conflict_count
@@ -1054,4 +1055,416 @@ RETURNING *;
-- name: GetMediaItemByKoboContentId :one
SELECT * FROM media_items WHERE kobo_content_id = $1;
-- Media Items Admin Operations
-- Media Items Admin Operations
-- ============================================
-- PHASE 1: UNIVERSAL BOOK IDENTIFIERS (Week 1)
-- ============================================
-- Update media item with universal identifiers
-- name: UpdateMediaItemIdentifiers :one
UPDATE media_items
SET
file_sha256 = $2,
opf_identifier = $3,
opf_uuid = $4,
hash_confidence = $5,
updated_at = NOW()
WHERE id = $1
RETURNING *;
-- Get media item by SHA-256 hash
-- name: GetMediaItemBySHA256 :one
SELECT * FROM media_items WHERE file_sha256 = $1;
-- Get media item by OPF identifier
-- name: GetMediaItemByOPFIdentifier :one
SELECT * FROM media_items WHERE opf_identifier = $1;
-- Get media item by OPF UUID
-- name: GetMediaItemByOPFUUID :one
SELECT * FROM media_items WHERE opf_uuid = $1;
-- Query media items by multiple identifiers with confidence scoring
-- name: QueryMediaItemsByIdentifiers :many
SELECT
mi.id,
mi.file_sha256,
mi.opf_identifier,
mi.opf_uuid,
mi.isbn,
mi.asin,
mi.title,
mi.author,
mi.file_size,
mi.hash_confidence,
CASE
WHEN $1::uuid IS NOT NULL AND mi.id = $1::uuid THEN 1.0
WHEN mi.opf_uuid IS NOT NULL AND mi.opf_uuid = $2::text THEN 0.95
WHEN mi.file_sha256 IS NOT NULL AND mi.file_sha256 = $3::text THEN 0.9
WHEN mi.opf_identifier IS NOT NULL AND mi.opf_identifier = $4::text THEN 0.85
WHEN mi.isbn IS NOT NULL AND mi.isbn = $5::text THEN 0.8
WHEN mi.asin IS NOT NULL AND mi.asin = $6::text THEN 0.8
ELSE 0.5
END as confidence_score
FROM media_items mi
WHERE
($1::uuid IS NULL OR mi.id = $1::uuid)
OR ($2::text IS NULL OR mi.opf_uuid = $2::text)
OR ($3::text IS NULL OR mi.file_sha256 = $3::text)
OR ($4::text IS NULL OR mi.opf_identifier = $4::text)
OR ($5::text IS NULL OR mi.isbn = $5::text)
OR ($6::text IS NULL OR mi.asin = $6::text)
OR ($7::text IS NULL OR mi.title ILIKE '%' || $7::text || '%')
ORDER BY confidence_score DESC;
-- MEDIA ITEM FORMATS QUERIES
-- Create media item format
-- name: CreateMediaItemFormat :one
INSERT INTO media_item_formats (media_item_id, format_type, file_path, file_sha256, file_size_bytes, mime_type, converted_from_format_id)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *;
-- Get media item formats
-- name: GetMediaItemFormats :many
SELECT * FROM media_item_formats WHERE media_item_id = $1;
-- Get media item format by type
-- name: GetMediaItemFormatByType :one
SELECT * FROM media_item_formats WHERE media_item_id = $1 AND format_type = $2;
-- Get media item format by SHA-256
-- name: GetMediaItemFormatBySHA256 :one
SELECT * FROM media_item_formats WHERE file_sha256 = $1;
-- Update media item format
-- name: UpdateMediaItemFormat :one
UPDATE media_item_formats
SET
file_path = $2,
file_sha256 = $3,
file_size_bytes = $4,
mime_type = $5
WHERE id = $1
RETURNING *;
-- Delete media item format
-- name: DeleteMediaItemFormat :exec
DELETE FROM media_item_formats WHERE id = $1;
-- DEVICE FILE ALIASES QUERIES
-- Create device file alias
-- name: CreateDeviceFileAlias :one
INSERT INTO device_file_aliases (media_item_id, device_id, file_path, file_sha256, confidence_score)
VALUES ($1, $2, $3, $4, $5)
RETURNING *;
-- Get device file alias
-- name: GetDeviceFileAlias :one
SELECT * FROM device_file_aliases WHERE device_id = $1 AND file_path = $2;
-- Get device file aliases by device
-- name: GetDeviceFileAliasesByDevice :many
SELECT dfa.*, mi.title, mi.author
FROM device_file_aliases dfa
JOIN media_items mi ON dfa.media_item_id = mi.id
WHERE dfa.device_id = $1
ORDER BY dfa.last_seen_at DESC;
-- Get device file alias by SHA-256
-- name: GetDeviceFileAliasBySHA256 :one
SELECT dfa.*, mi.title, mi.author
FROM device_file_aliases dfa
JOIN media_items mi ON dfa.media_item_id = mi.id
WHERE dfa.file_sha256 = $1;
-- Update device file alias
-- name: UpdateDeviceFileAlias :one
UPDATE device_file_aliases
SET
media_item_id = $2,
file_sha256 = $3,
confidence_score = $4,
last_seen_at = NOW()
WHERE id = $1
RETURNING *;
-- Delete device file alias
-- name: DeleteDeviceFileAlias :exec
DELETE FROM device_file_aliases WHERE id = $1;
-- COLLECTIONS QUERIES
-- Create collection
-- name: CreateCollection :one
INSERT INTO collections (user_id, name, description, color, icon, auto_assign_rules, view_settings)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *;
-- Get collection
-- name: GetCollection :one
SELECT * FROM collections WHERE id = $1;
-- Get collections by user
-- name: GetCollectionsByUser :many
SELECT * FROM collections WHERE user_id = $1 ORDER BY created_at DESC;
-- Get collection with book count
-- name: GetCollectionWithBookCount :one
SELECT
c.*,
COUNT(ci.id) as book_count
FROM collections c
LEFT JOIN collection_items ci ON c.id = ci.collection_id
WHERE c.id = $1
GROUP BY c.id;
-- Update collection
-- name: UpdateCollection :one
UPDATE collections
SET
name = $2,
description = $3,
color = $4,
icon = $5,
auto_assign_rules = $6,
view_settings = $7
WHERE id = $1
RETURNING *;
-- Delete collection
-- name: DeleteCollection :exec
DELETE FROM collections WHERE id = $1;
-- COLLECTION ITEMS QUERIES
-- Add book to collection
-- name: AddBookToCollection :one
INSERT INTO collection_items (collection_id, media_item_id, added_by_user_id)
VALUES ($1, $2, $3)
ON CONFLICT (collection_id, media_item_id) DO NOTHING
RETURNING *;
-- Remove book from collection
-- name: RemoveBookFromCollection :exec
DELETE FROM collection_items WHERE collection_id = $1 AND media_item_id = $2;
-- Get collection items
-- name: GetCollectionItems :many
SELECT ci.*, mi.title, mi.author, mi.cover_image_path
FROM collection_items ci
JOIN media_items mi ON ci.media_item_id = mi.id
WHERE ci.collection_id = $1
ORDER BY ci.added_at DESC;
-- Get collections for book
-- name: GetCollectionsForBook :many
SELECT c.*
FROM collections c
JOIN collection_items ci ON c.id = ci.collection_id
WHERE ci.media_item_id = $1;
-- Check if book is in collection
-- name: IsBookInCollection :one
SELECT EXISTS(
SELECT 1 FROM collection_items
WHERE collection_id = $1 AND media_item_id = $2
) as in_collection;
-- DEVICE SHELF MAPPINGS QUERIES
-- Create device shelf mapping
-- name: CreateDeviceShelfMapping :one
INSERT INTO device_shelf_mappings (collection_id, device_id, device_shelf_name, sync_direction)
VALUES ($1, $2, $3, $4)
ON CONFLICT (collection_id, device_id)
DO UPDATE SET
device_shelf_name = EXCLUDED.device_shelf_name,
sync_direction = EXCLUDED.sync_direction
RETURNING *;
-- Get device shelf mappings
-- name: GetDeviceShelfMappings :many
SELECT dsm.*, c.name as collection_name, c.icon as collection_icon
FROM device_shelf_mappings dsm
JOIN collections c ON dsm.collection_id = c.id
WHERE dsm.device_id = $1;
-- Get device shelf mapping
-- name: GetDeviceShelfMapping :one
SELECT * FROM device_shelf_mappings WHERE device_id = $1 AND collection_id = $2;
-- Update device shelf mapping
-- name: UpdateDeviceShelfMapping :one
UPDATE device_shelf_mappings
SET
device_shelf_name = $2,
sync_direction = $3
WHERE id = $1
RETURNING *;
-- Delete device shelf mapping
-- name: DeleteDeviceShelfMapping :exec
DELETE FROM device_shelf_mappings WHERE id = $1;
-- DEVICE CATALOGS QUERIES
-- Create device catalog entry
-- name: CreateDeviceCatalog :one
INSERT INTO device_catalogs (device_id, media_item_id, bookmann_uuid, kobo_content_id, content_id_type, available, delivery_date, delivery_method)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (device_id, kobo_content_id)
DO UPDATE SET
available = EXCLUDED.available,
delivery_date = COALESCE(EXCLUDED.delivery_date, device_catalogs.delivery_date),
delivery_method = EXCLUDED.delivery_method
RETURNING *;
-- Get device catalog by Bookmann UUID
-- name: GetDeviceCatalogByBookmannUUID :one
SELECT * FROM device_catalogs WHERE device_id = $1 AND bookmann_uuid = $2;
-- Get device catalog by Kobo ContentId
-- name: GetDeviceCatalogByKoboContentId :one
SELECT * FROM device_catalogs WHERE kobo_content_id = $1;
-- Get device catalog entries
-- name: GetDeviceCatalogEntries :many
SELECT dc.*, mi.title, mi.author
FROM device_catalogs dc
JOIN media_items mi ON dc.media_item_id = mi.id
WHERE dc.device_id = $1 AND dc.available = true
ORDER BY dc.delivery_date DESC;
-- Update device catalog availability
-- name: UpdateDeviceCatalogAvailability :exec
UPDATE device_catalogs
SET available = $2
WHERE id = $1;
-- Delete device catalog entry
-- name: DeleteDeviceCatalog :exec
DELETE FROM device_catalogs WHERE id = $1;
-- SYSTEM CONFIG QUERIES
-- Get system config
-- name: GetSystemConfig :one
SELECT * FROM system_config WHERE key = $1;
-- Get all system config
-- name: GetAllSystemConfig :many
SELECT * FROM system_config ORDER BY key;
-- Set system config
-- name: SetSystemConfig :one
INSERT INTO system_config (key, value, updated_by)
VALUES ($1, $2, $3)
ON CONFLICT (key)
DO UPDATE SET
value = EXCLUDED.value,
updated_by = EXCLUDED.updated_by,
updated_at = NOW()
RETURNING *;
-- Delete system config
-- name: DeleteSystemConfig :exec
DELETE FROM system_config WHERE key = $1;
-- OPDS TOKENS QUERIES
-- Create OPDS token
-- name: CreateOpdsToken :one
INSERT INTO opds_tokens (device_id, token, token_type, expires_at)
VALUES ($1, $2, $3, $4)
RETURNING *;
-- Get OPDS token
-- name: GetOpdsToken :one
SELECT ot.*, d.device_name, d.device_type
FROM opds_tokens ot
JOIN devices d ON ot.device_id = d.id
WHERE ot.token = $1 AND ot.expires_at > NOW();
-- Get OPDS tokens by device
-- name: GetOpdsTokensByDevice :many
SELECT * FROM opds_tokens WHERE device_id = $1 AND expires_at > NOW();
-- Revoke OPDS token
-- name: RevokeOpdsToken :exec
DELETE FROM opds_tokens WHERE token = $1;
-- Cleanup expired OPDS tokens
-- name: CleanupExpiredOpdsTokens :exec
DELETE FROM opds_tokens WHERE expires_at < NOW();
-- Update Kobo shelves to support collections
-- name: UpdateKoboShelfCollection :one
UPDATE kobo_shelves
SET
collection_id = $2,
position_in_collection = $3,
last_synced_at = NOW()
WHERE id = $1
RETURNING *;
-- Get Kobo shelves by collection
-- name: GetKoboShelvesByCollection :many
SELECT ks.*, mi.title, mi.author
FROM kobo_shelves ks
JOIN media_items mi ON ks.media_item_id = mi.id
WHERE ks.device_id = $1 AND ks.collection_id = $2
ORDER BY ks.position_in_collection ASC, ks.shelf_position ASC;
-- ============================================
-- PHASE 6: ENHANCED KOBO SYNC (Week 3-4)
-- ============================================
-- Create unlinked book entry
-- name: CreateUnlinkedBook :one
INSERT INTO unlinked_books (device_id, content_id, file_path, title, author, confidence_score)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *;
-- Get unlinked books for a device
-- name: GetUnlinkedBooksByDevice :many
SELECT ub.*, d.device_name, d.device_type
FROM unlinked_books ub
JOIN devices d ON ub.device_id = d.id
WHERE ub.device_id = $1 AND ub.resolved = false
ORDER BY ub.last_seen_at DESC;
-- Get unlinked book by ContentId
-- name: GetUnlinkedBookByContentId :one
SELECT * FROM unlinked_books WHERE device_id = $1 AND content_id = $2;
-- Resolve unlinked book
-- name: ResolveUnlinkedBook :one
UPDATE unlinked_books
SET
resolved = true,
media_item_id = $2,
resolved_at = NOW(),
resolution_method = $3
WHERE id = $1
RETURNING *;
-- Count unlinked books for a device
-- name: CountUnlinkedBooks :one
SELECT COUNT(*) as count
FROM unlinked_books
WHERE device_id = $1 AND resolved = false;
-- Link unlinked book to media item
-- name: LinkUnlinkedBook :one
UPDATE unlinked_books
SET
media_item_id = $2,
confidence_score = $3,
resolved = true,
resolved_at = NOW(),
resolution_method = 'manual_link'
WHERE id = $1
RETURNING *;