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
+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 *;