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);