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:
@@ -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 *;
|
||||
Reference in New Issue
Block a user