nix: set up project-wide formatter

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I4806c58aa0a17f504c9312723ad770166a6a6964
This commit is contained in:
raf 2026-03-22 23:42:02 +03:00
commit 9e5eb41d39
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
78 changed files with 7406 additions and 2504 deletions

View file

@ -1,19 +1,19 @@
-- Add file_mtime column to media_items table for incremental scanning
-- Stores Unix timestamp in seconds of the file's modification time
ALTER TABLE media_items ADD COLUMN file_mtime BIGINT;
ALTER TABLE media_items
ADD COLUMN file_mtime BIGINT;
-- Create index for quick mtime lookups
CREATE INDEX IF NOT EXISTS idx_media_items_file_mtime ON media_items(file_mtime);
CREATE INDEX IF NOT EXISTS idx_media_items_file_mtime ON media_items (file_mtime);
-- Create a scan_history table to track when each directory was last scanned
CREATE TABLE IF NOT EXISTS scan_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
directory TEXT NOT NULL UNIQUE,
last_scan_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
files_scanned INTEGER NOT NULL DEFAULT 0,
files_changed INTEGER NOT NULL DEFAULT 0,
scan_duration_ms INTEGER
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
directory TEXT NOT NULL UNIQUE,
last_scan_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
files_scanned INTEGER NOT NULL DEFAULT 0,
files_changed INTEGER NOT NULL DEFAULT 0,
scan_duration_ms INTEGER
);
CREATE INDEX IF NOT EXISTS idx_scan_history_directory ON scan_history(directory);
CREATE INDEX IF NOT EXISTS idx_scan_history_directory ON scan_history (directory);

View file

@ -1,18 +1,17 @@
-- Session persistence for database-backed sessions
-- Replaces in-memory session storage
CREATE TABLE IF NOT EXISTS sessions (
session_token TEXT PRIMARY KEY NOT NULL,
user_id TEXT,
username TEXT NOT NULL,
role TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
last_accessed TIMESTAMPTZ NOT NULL
session_token TEXT PRIMARY KEY NOT NULL,
user_id TEXT,
username TEXT NOT NULL,
role TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
last_accessed TIMESTAMPTZ NOT NULL
);
-- Index for efficient cleanup of expired sessions
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions (expires_at);
-- Index for listing sessions by username
CREATE INDEX IF NOT EXISTS idx_sessions_username ON sessions(username);
CREATE INDEX IF NOT EXISTS idx_sessions_username ON sessions (username);

View file

@ -1,60 +1,61 @@
-- V12: Book Management Schema (PostgreSQL)
-- Adds comprehensive book metadata tracking, authors, and identifiers
-- Book metadata (supplements media_items for EPUB/PDF/MOBI)
CREATE TABLE book_metadata (
media_id UUID PRIMARY KEY REFERENCES media_items(id) ON DELETE CASCADE,
isbn TEXT,
isbn13 TEXT, -- Normalized ISBN-13 for lookups
publisher TEXT,
language TEXT, -- ISO 639-1 code
page_count INTEGER,
publication_date DATE,
series_name TEXT,
series_index DOUBLE PRECISION, -- Supports 1.5, etc.
format TEXT, -- 'epub', 'pdf', 'mobi', 'azw3'
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
media_id UUID PRIMARY KEY REFERENCES media_items (id) ON DELETE CASCADE,
isbn TEXT,
isbn13 TEXT, -- Normalized ISBN-13 for lookups
publisher TEXT,
language TEXT, -- ISO 639-1 code
page_count INTEGER,
publication_date DATE,
series_name TEXT,
series_index DOUBLE PRECISION, -- Supports 1.5, etc.
format TEXT, -- 'epub', 'pdf', 'mobi', 'azw3'
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_book_isbn13 ON book_metadata(isbn13);
CREATE INDEX idx_book_series ON book_metadata(series_name, series_index);
CREATE INDEX idx_book_publisher ON book_metadata(publisher);
CREATE INDEX idx_book_language ON book_metadata(language);
CREATE INDEX idx_book_isbn13 ON book_metadata (isbn13);
CREATE INDEX idx_book_series ON book_metadata (series_name, series_index);
CREATE INDEX idx_book_publisher ON book_metadata (publisher);
CREATE INDEX idx_book_language ON book_metadata (language);
-- Multiple authors per book (many-to-many)
CREATE TABLE book_authors (
media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
author_name TEXT NOT NULL,
author_sort TEXT, -- "Last, First" for sorting
role TEXT NOT NULL DEFAULT 'author', -- author, translator, editor, illustrator
position INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (media_id, author_name, role)
media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE,
author_name TEXT NOT NULL,
author_sort TEXT, -- "Last, First" for sorting
role TEXT NOT NULL DEFAULT 'author', -- author, translator, editor, illustrator
position INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (media_id, author_name, role)
);
CREATE INDEX idx_book_authors_name ON book_authors(author_name);
CREATE INDEX idx_book_authors_sort ON book_authors(author_sort);
CREATE INDEX idx_book_authors_name ON book_authors (author_name);
CREATE INDEX idx_book_authors_sort ON book_authors (author_sort);
-- Multiple identifiers (ISBN variants, ASIN, DOI, etc.)
CREATE TABLE book_identifiers (
media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
identifier_type TEXT NOT NULL, -- isbn, isbn13, asin, doi, lccn, oclc
identifier_value TEXT NOT NULL,
PRIMARY KEY (media_id, identifier_type, identifier_value)
media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE,
identifier_type TEXT NOT NULL, -- isbn, isbn13, asin, doi, lccn, oclc
identifier_value TEXT NOT NULL,
PRIMARY KEY (media_id, identifier_type, identifier_value)
);
CREATE INDEX idx_book_identifiers ON book_identifiers(identifier_type, identifier_value);
CREATE INDEX idx_book_identifiers ON book_identifiers (identifier_type, identifier_value);
-- Trigger to update updated_at on book_metadata changes
CREATE OR REPLACE FUNCTION update_book_metadata_timestamp()
RETURNS TRIGGER AS $$
CREATE OR REPLACE FUNCTION update_book_metadata_timestamp () RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_book_metadata_timestamp
BEFORE UPDATE ON book_metadata
FOR EACH ROW
EXECUTE FUNCTION update_book_metadata_timestamp();
CREATE TRIGGER update_book_metadata_timestamp BEFORE
UPDATE ON book_metadata FOR EACH ROW
EXECUTE FUNCTION update_book_metadata_timestamp ();

View file

@ -1,15 +1,40 @@
-- V13: Enhanced photo metadata support
-- Add photo-specific fields to media_items table
ALTER TABLE media_items
ADD COLUMN date_taken TIMESTAMPTZ;
ALTER TABLE media_items ADD COLUMN date_taken TIMESTAMPTZ;
ALTER TABLE media_items ADD COLUMN latitude DOUBLE PRECISION;
ALTER TABLE media_items ADD COLUMN longitude DOUBLE PRECISION;
ALTER TABLE media_items ADD COLUMN camera_make TEXT;
ALTER TABLE media_items ADD COLUMN camera_model TEXT;
ALTER TABLE media_items ADD COLUMN rating INTEGER CHECK (rating >= 0 AND rating <= 5);
ALTER TABLE media_items
ADD COLUMN latitude DOUBLE PRECISION;
ALTER TABLE media_items
ADD COLUMN longitude DOUBLE PRECISION;
ALTER TABLE media_items
ADD COLUMN camera_make TEXT;
ALTER TABLE media_items
ADD COLUMN camera_model TEXT;
ALTER TABLE media_items
ADD COLUMN rating INTEGER CHECK (
rating >= 0
AND rating <= 5
);
-- Indexes for photo queries
CREATE INDEX idx_media_date_taken ON media_items(date_taken) WHERE date_taken IS NOT NULL;
CREATE INDEX idx_media_location ON media_items(latitude, longitude) WHERE latitude IS NOT NULL AND longitude IS NOT NULL;
CREATE INDEX idx_media_camera ON media_items(camera_make) WHERE camera_make IS NOT NULL;
CREATE INDEX idx_media_rating ON media_items(rating) WHERE rating IS NOT NULL;
CREATE INDEX idx_media_date_taken ON media_items (date_taken)
WHERE
date_taken IS NOT NULL;
CREATE INDEX idx_media_location ON media_items (latitude, longitude)
WHERE
latitude IS NOT NULL
AND longitude IS NOT NULL;
CREATE INDEX idx_media_camera ON media_items (camera_make)
WHERE
camera_make IS NOT NULL;
CREATE INDEX idx_media_rating ON media_items (rating)
WHERE
rating IS NOT NULL;

View file

@ -1,7 +1,9 @@
-- V14: Perceptual hash for duplicate detection
-- Add perceptual hash column for image similarity detection
ALTER TABLE media_items ADD COLUMN perceptual_hash TEXT;
ALTER TABLE media_items
ADD COLUMN perceptual_hash TEXT;
-- Index for perceptual hash lookups
CREATE INDEX idx_media_phash ON media_items(perceptual_hash) WHERE perceptual_hash IS NOT NULL;
CREATE INDEX idx_media_phash ON media_items (perceptual_hash)
WHERE
perceptual_hash IS NOT NULL;

View file

@ -1,30 +1,33 @@
-- V15: Managed File Storage
-- Adds server-side content-addressable storage for uploaded files
-- Add storage mode to media_items (external = file on disk, managed = in content-addressable storage)
ALTER TABLE media_items ADD COLUMN storage_mode TEXT NOT NULL DEFAULT 'external';
ALTER TABLE media_items
ADD COLUMN storage_mode TEXT NOT NULL DEFAULT 'external';
-- Original filename for managed uploads (preserved separately from file_name which may be normalized)
ALTER TABLE media_items ADD COLUMN original_filename TEXT;
ALTER TABLE media_items
ADD COLUMN original_filename TEXT;
-- When the file was uploaded to managed storage
ALTER TABLE media_items ADD COLUMN uploaded_at TIMESTAMPTZ;
ALTER TABLE media_items
ADD COLUMN uploaded_at TIMESTAMPTZ;
-- Storage key for looking up the blob (usually same as content_hash for deduplication)
ALTER TABLE media_items ADD COLUMN storage_key TEXT;
ALTER TABLE media_items
ADD COLUMN storage_key TEXT;
-- Managed blobs table - tracks deduplicated file storage
CREATE TABLE managed_blobs (
content_hash TEXT PRIMARY KEY NOT NULL,
file_size BIGINT NOT NULL,
mime_type TEXT NOT NULL,
reference_count INTEGER NOT NULL DEFAULT 1,
stored_at TIMESTAMPTZ NOT NULL,
last_verified TIMESTAMPTZ
content_hash TEXT PRIMARY KEY NOT NULL,
file_size BIGINT NOT NULL,
mime_type TEXT NOT NULL,
reference_count INTEGER NOT NULL DEFAULT 1,
stored_at TIMESTAMPTZ NOT NULL,
last_verified TIMESTAMPTZ
);
-- Index for finding managed media items
CREATE INDEX idx_media_storage_mode ON media_items(storage_mode);
CREATE INDEX idx_media_storage_mode ON media_items (storage_mode);
-- Index for finding orphaned blobs (reference_count = 0)
CREATE INDEX idx_blobs_reference_count ON managed_blobs(reference_count);
CREATE INDEX idx_blobs_reference_count ON managed_blobs (reference_count);

View file

@ -1,91 +1,100 @@
-- V16: Cross-Device Sync System
-- Adds device registration, change tracking, and chunked upload support
-- Sync devices table
CREATE TABLE sync_devices (
id TEXT PRIMARY KEY NOT NULL,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
device_type TEXT NOT NULL,
client_version TEXT NOT NULL,
os_info TEXT,
device_token_hash TEXT NOT NULL UNIQUE,
last_sync_at TIMESTAMPTZ,
last_seen_at TIMESTAMPTZ NOT NULL,
sync_cursor BIGINT DEFAULT 0,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
id TEXT PRIMARY KEY NOT NULL,
user_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
name TEXT NOT NULL,
device_type TEXT NOT NULL,
client_version TEXT NOT NULL,
os_info TEXT,
device_token_hash TEXT NOT NULL UNIQUE,
last_sync_at TIMESTAMPTZ,
last_seen_at TIMESTAMPTZ NOT NULL,
sync_cursor BIGINT DEFAULT 0,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX idx_sync_devices_user ON sync_devices(user_id);
CREATE INDEX idx_sync_devices_token ON sync_devices(device_token_hash);
CREATE INDEX idx_sync_devices_user ON sync_devices (user_id);
CREATE INDEX idx_sync_devices_token ON sync_devices (device_token_hash);
-- Sync log table - tracks all changes for sync
CREATE TABLE sync_log (
id TEXT PRIMARY KEY NOT NULL,
sequence BIGSERIAL UNIQUE NOT NULL,
change_type TEXT NOT NULL,
media_id TEXT REFERENCES media_items(id) ON DELETE SET NULL,
path TEXT NOT NULL,
content_hash TEXT,
file_size BIGINT,
metadata_json TEXT,
changed_by_device TEXT REFERENCES sync_devices(id) ON DELETE SET NULL,
timestamp TIMESTAMPTZ NOT NULL
id TEXT PRIMARY KEY NOT NULL,
sequence BIGSERIAL UNIQUE NOT NULL,
change_type TEXT NOT NULL,
media_id TEXT REFERENCES media_items (id) ON DELETE SET NULL,
path TEXT NOT NULL,
content_hash TEXT,
file_size BIGINT,
metadata_json TEXT,
changed_by_device TEXT REFERENCES sync_devices (id) ON DELETE SET NULL,
timestamp TIMESTAMPTZ NOT NULL
);
CREATE INDEX idx_sync_log_sequence ON sync_log(sequence);
CREATE INDEX idx_sync_log_path ON sync_log(path);
CREATE INDEX idx_sync_log_timestamp ON sync_log(timestamp);
CREATE INDEX idx_sync_log_sequence ON sync_log (sequence);
CREATE INDEX idx_sync_log_path ON sync_log (path);
CREATE INDEX idx_sync_log_timestamp ON sync_log (timestamp);
-- Sequence counter for sync log
CREATE TABLE sync_sequence (
id INTEGER PRIMARY KEY CHECK (id = 1),
current_value BIGINT NOT NULL DEFAULT 0
id INTEGER PRIMARY KEY CHECK (id = 1),
current_value BIGINT NOT NULL DEFAULT 0
);
INSERT INTO sync_sequence (id, current_value) VALUES (1, 0);
INSERT INTO
sync_sequence (id, current_value)
VALUES
(1, 0);
-- Device sync state - tracks sync status per device per file
CREATE TABLE device_sync_state (
device_id TEXT NOT NULL REFERENCES sync_devices(id) ON DELETE CASCADE,
path TEXT NOT NULL,
local_hash TEXT,
server_hash TEXT,
local_mtime BIGINT,
server_mtime BIGINT,
sync_status TEXT NOT NULL,
last_synced_at TIMESTAMPTZ,
conflict_info_json TEXT,
PRIMARY KEY (device_id, path)
device_id TEXT NOT NULL REFERENCES sync_devices (id) ON DELETE CASCADE,
path TEXT NOT NULL,
local_hash TEXT,
server_hash TEXT,
local_mtime BIGINT,
server_mtime BIGINT,
sync_status TEXT NOT NULL,
last_synced_at TIMESTAMPTZ,
conflict_info_json TEXT,
PRIMARY KEY (device_id, path)
);
CREATE INDEX idx_device_sync_status ON device_sync_state(device_id, sync_status);
CREATE INDEX idx_device_sync_status ON device_sync_state (device_id, sync_status);
-- Upload sessions for chunked uploads
CREATE TABLE upload_sessions (
id TEXT PRIMARY KEY NOT NULL,
device_id TEXT NOT NULL REFERENCES sync_devices(id) ON DELETE CASCADE,
target_path TEXT NOT NULL,
expected_hash TEXT NOT NULL,
expected_size BIGINT NOT NULL,
chunk_size BIGINT NOT NULL,
chunk_count BIGINT NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
last_activity TIMESTAMPTZ NOT NULL
id TEXT PRIMARY KEY NOT NULL,
device_id TEXT NOT NULL REFERENCES sync_devices (id) ON DELETE CASCADE,
target_path TEXT NOT NULL,
expected_hash TEXT NOT NULL,
expected_size BIGINT NOT NULL,
chunk_size BIGINT NOT NULL,
chunk_count BIGINT NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
last_activity TIMESTAMPTZ NOT NULL
);
CREATE INDEX idx_upload_sessions_device ON upload_sessions(device_id);
CREATE INDEX idx_upload_sessions_status ON upload_sessions(status);
CREATE INDEX idx_upload_sessions_expires ON upload_sessions(expires_at);
CREATE INDEX idx_upload_sessions_device ON upload_sessions (device_id);
CREATE INDEX idx_upload_sessions_status ON upload_sessions (status);
CREATE INDEX idx_upload_sessions_expires ON upload_sessions (expires_at);
-- Upload chunks - tracks received chunks
CREATE TABLE upload_chunks (
upload_id TEXT NOT NULL REFERENCES upload_sessions(id) ON DELETE CASCADE,
chunk_index BIGINT NOT NULL,
offset BIGINT NOT NULL,
upload_id TEXT NOT NULL REFERENCES upload_sessions (id) ON DELETE CASCADE,
chunk_index BIGINT NOT NULL,
offset
BIGINT NOT NULL,
size BIGINT NOT NULL,
hash TEXT NOT NULL,
received_at TIMESTAMPTZ NOT NULL,
@ -94,17 +103,20 @@ CREATE TABLE upload_chunks (
-- Sync conflicts
CREATE TABLE sync_conflicts (
id TEXT PRIMARY KEY NOT NULL,
device_id TEXT NOT NULL REFERENCES sync_devices(id) ON DELETE CASCADE,
path TEXT NOT NULL,
local_hash TEXT NOT NULL,
local_mtime BIGINT NOT NULL,
server_hash TEXT NOT NULL,
server_mtime BIGINT NOT NULL,
detected_at TIMESTAMPTZ NOT NULL,
resolved_at TIMESTAMPTZ,
resolution TEXT
id TEXT PRIMARY KEY NOT NULL,
device_id TEXT NOT NULL REFERENCES sync_devices (id) ON DELETE CASCADE,
path TEXT NOT NULL,
local_hash TEXT NOT NULL,
local_mtime BIGINT NOT NULL,
server_hash TEXT NOT NULL,
server_mtime BIGINT NOT NULL,
detected_at TIMESTAMPTZ NOT NULL,
resolved_at TIMESTAMPTZ,
resolution TEXT
);
CREATE INDEX idx_sync_conflicts_device ON sync_conflicts(device_id);
CREATE INDEX idx_sync_conflicts_unresolved ON sync_conflicts(device_id) WHERE resolved_at IS NULL;
CREATE INDEX idx_sync_conflicts_device ON sync_conflicts (device_id);
CREATE INDEX idx_sync_conflicts_unresolved ON sync_conflicts (device_id)
WHERE
resolved_at IS NULL;

View file

@ -1,68 +1,85 @@
-- V17: Enhanced Sharing System
-- Replaces simple share_links with comprehensive sharing capabilities
-- Enhanced shares table
CREATE TABLE shares (
id TEXT PRIMARY KEY NOT NULL,
target_type TEXT NOT NULL CHECK (target_type IN ('media', 'collection', 'tag', 'saved_search')),
target_id TEXT NOT NULL,
owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
recipient_type TEXT NOT NULL CHECK (recipient_type IN ('public_link', 'user', 'group', 'federated')),
recipient_user_id TEXT REFERENCES users(id) ON DELETE CASCADE,
recipient_group_id TEXT,
recipient_federated_handle TEXT,
recipient_federated_server TEXT,
public_token TEXT UNIQUE,
public_password_hash TEXT,
perm_view BOOLEAN NOT NULL DEFAULT TRUE,
perm_download BOOLEAN NOT NULL DEFAULT FALSE,
perm_edit BOOLEAN NOT NULL DEFAULT FALSE,
perm_delete BOOLEAN NOT NULL DEFAULT FALSE,
perm_reshare BOOLEAN NOT NULL DEFAULT FALSE,
perm_add BOOLEAN NOT NULL DEFAULT FALSE,
note TEXT,
expires_at TIMESTAMPTZ,
access_count BIGINT NOT NULL DEFAULT 0,
last_accessed TIMESTAMPTZ,
inherit_to_children BOOLEAN NOT NULL DEFAULT TRUE,
parent_share_id TEXT REFERENCES shares(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
UNIQUE(owner_id, target_type, target_id, recipient_type, recipient_user_id)
id TEXT PRIMARY KEY NOT NULL,
target_type TEXT NOT NULL CHECK (
target_type IN ('media', 'collection', 'tag', 'saved_search')
),
target_id TEXT NOT NULL,
owner_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
recipient_type TEXT NOT NULL CHECK (
recipient_type IN ('public_link', 'user', 'group', 'federated')
),
recipient_user_id TEXT REFERENCES users (id) ON DELETE CASCADE,
recipient_group_id TEXT,
recipient_federated_handle TEXT,
recipient_federated_server TEXT,
public_token TEXT UNIQUE,
public_password_hash TEXT,
perm_view BOOLEAN NOT NULL DEFAULT TRUE,
perm_download BOOLEAN NOT NULL DEFAULT FALSE,
perm_edit BOOLEAN NOT NULL DEFAULT FALSE,
perm_delete BOOLEAN NOT NULL DEFAULT FALSE,
perm_reshare BOOLEAN NOT NULL DEFAULT FALSE,
perm_add BOOLEAN NOT NULL DEFAULT FALSE,
note TEXT,
expires_at TIMESTAMPTZ,
access_count BIGINT NOT NULL DEFAULT 0,
last_accessed TIMESTAMPTZ,
inherit_to_children BOOLEAN NOT NULL DEFAULT TRUE,
parent_share_id TEXT REFERENCES shares (id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
UNIQUE (
owner_id,
target_type,
target_id,
recipient_type,
recipient_user_id
)
);
CREATE INDEX idx_shares_owner ON shares(owner_id);
CREATE INDEX idx_shares_recipient_user ON shares(recipient_user_id);
CREATE INDEX idx_shares_target ON shares(target_type, target_id);
CREATE INDEX idx_shares_token ON shares(public_token);
CREATE INDEX idx_shares_expires ON shares(expires_at);
CREATE INDEX idx_shares_owner ON shares (owner_id);
CREATE INDEX idx_shares_recipient_user ON shares (recipient_user_id);
CREATE INDEX idx_shares_target ON shares (target_type, target_id);
CREATE INDEX idx_shares_token ON shares (public_token);
CREATE INDEX idx_shares_expires ON shares (expires_at);
-- Share activity log
CREATE TABLE share_activity (
id TEXT PRIMARY KEY NOT NULL,
share_id TEXT NOT NULL REFERENCES shares(id) ON DELETE CASCADE,
actor_id TEXT REFERENCES users(id) ON DELETE SET NULL,
actor_ip TEXT,
action TEXT NOT NULL,
details TEXT,
timestamp TIMESTAMPTZ NOT NULL
id TEXT PRIMARY KEY NOT NULL,
share_id TEXT NOT NULL REFERENCES shares (id) ON DELETE CASCADE,
actor_id TEXT REFERENCES users (id) ON DELETE SET NULL,
actor_ip TEXT,
action TEXT NOT NULL,
details TEXT,
timestamp TIMESTAMPTZ NOT NULL
);
CREATE INDEX idx_share_activity_share ON share_activity(share_id);
CREATE INDEX idx_share_activity_timestamp ON share_activity(timestamp);
CREATE INDEX idx_share_activity_share ON share_activity (share_id);
CREATE INDEX idx_share_activity_timestamp ON share_activity (timestamp);
-- Share notifications
CREATE TABLE share_notifications (
id TEXT PRIMARY KEY NOT NULL,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
share_id TEXT NOT NULL REFERENCES shares(id) ON DELETE CASCADE,
notification_type TEXT NOT NULL,
is_read BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL
id TEXT PRIMARY KEY NOT NULL,
user_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
share_id TEXT NOT NULL REFERENCES shares (id) ON DELETE CASCADE,
notification_type TEXT NOT NULL,
is_read BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX idx_share_notifications_user ON share_notifications(user_id);
CREATE INDEX idx_share_notifications_unread ON share_notifications(user_id) WHERE is_read = FALSE;
CREATE INDEX idx_share_notifications_user ON share_notifications (user_id);
CREATE INDEX idx_share_notifications_unread ON share_notifications (user_id)
WHERE
is_read = FALSE;
-- Migrate existing share_links to new shares table
DO $$

View file

@ -1,11 +1,13 @@
-- V18: File Management (Rename, Move, Trash)
-- Adds soft delete support for trash/recycle bin functionality
-- Add deleted_at column for soft delete (trash)
ALTER TABLE media_items ADD COLUMN deleted_at TIMESTAMPTZ;
ALTER TABLE media_items
ADD COLUMN deleted_at TIMESTAMPTZ;
-- Index for efficient trash queries
CREATE INDEX idx_media_deleted_at ON media_items(deleted_at);
CREATE INDEX idx_media_deleted_at ON media_items (deleted_at);
-- Partial index for listing non-deleted items (most common query pattern)
CREATE INDEX idx_media_not_deleted ON media_items(id) WHERE deleted_at IS NULL;
CREATE INDEX idx_media_not_deleted ON media_items (id)
WHERE
deleted_at IS NULL;

View file

@ -1,35 +1,35 @@
-- V19: Markdown Links (Obsidian-style bidirectional links)
-- Adds support for wikilinks, markdown links, embeds, and backlink tracking
-- Table for storing extracted markdown links
CREATE TABLE IF NOT EXISTS markdown_links (
id TEXT PRIMARY KEY NOT NULL,
source_media_id TEXT NOT NULL,
target_path TEXT NOT NULL, -- raw link target (wikilink or path)
target_media_id TEXT, -- resolved media_id (nullable if unresolved)
link_type TEXT NOT NULL, -- 'wikilink', 'markdown_link', 'embed'
link_text TEXT, -- display text for the link
line_number INTEGER, -- line number in source file
context TEXT, -- surrounding text for preview
created_at TIMESTAMPTZ NOT NULL,
FOREIGN KEY (source_media_id) REFERENCES media_items(id) ON DELETE CASCADE,
FOREIGN KEY (target_media_id) REFERENCES media_items(id) ON DELETE SET NULL
id TEXT PRIMARY KEY NOT NULL,
source_media_id TEXT NOT NULL,
target_path TEXT NOT NULL, -- raw link target (wikilink or path)
target_media_id TEXT, -- resolved media_id (nullable if unresolved)
link_type TEXT NOT NULL, -- 'wikilink', 'markdown_link', 'embed'
link_text TEXT, -- display text for the link
line_number INTEGER, -- line number in source file
context TEXT, -- surrounding text for preview
created_at TIMESTAMPTZ NOT NULL,
FOREIGN KEY (source_media_id) REFERENCES media_items (id) ON DELETE CASCADE,
FOREIGN KEY (target_media_id) REFERENCES media_items (id) ON DELETE SET NULL
);
-- Index for efficient outgoing link queries (what does this note link to?)
CREATE INDEX idx_links_source ON markdown_links(source_media_id);
CREATE INDEX idx_links_source ON markdown_links (source_media_id);
-- Index for efficient backlink queries (what links to this note?)
CREATE INDEX idx_links_target ON markdown_links(target_media_id);
CREATE INDEX idx_links_target ON markdown_links (target_media_id);
-- Index for path-based resolution (finding unresolved links)
CREATE INDEX idx_links_target_path ON markdown_links(target_path);
CREATE INDEX idx_links_target_path ON markdown_links (target_path);
-- Index for link type filtering
CREATE INDEX idx_links_type ON markdown_links(link_type);
CREATE INDEX idx_links_type ON markdown_links (link_type);
-- Track when links were last extracted from a media item
ALTER TABLE media_items ADD COLUMN links_extracted_at TIMESTAMPTZ;
ALTER TABLE media_items
ADD COLUMN links_extracted_at TIMESTAMPTZ;
-- Index for finding media items that need link extraction
CREATE INDEX idx_media_links_extracted ON media_items(links_extracted_at);
CREATE INDEX idx_media_links_extracted ON media_items (links_extracted_at);

View file

@ -1,73 +1,75 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE TABLE IF NOT EXISTS root_dirs (
path TEXT PRIMARY KEY NOT NULL
);
CREATE TABLE IF NOT EXISTS root_dirs (path TEXT PRIMARY KEY NOT NULL);
CREATE TABLE IF NOT EXISTS media_items (
id UUID PRIMARY KEY NOT NULL,
path TEXT NOT NULL UNIQUE,
file_name TEXT NOT NULL,
media_type TEXT NOT NULL,
content_hash TEXT NOT NULL UNIQUE,
file_size BIGINT NOT NULL,
title TEXT,
artist TEXT,
album TEXT,
genre TEXT,
year INTEGER,
duration_secs DOUBLE PRECISION,
description TEXT,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
id UUID PRIMARY KEY NOT NULL,
path TEXT NOT NULL UNIQUE,
file_name TEXT NOT NULL,
media_type TEXT NOT NULL,
content_hash TEXT NOT NULL UNIQUE,
file_size BIGINT NOT NULL,
title TEXT,
artist TEXT,
album TEXT,
genre TEXT,
year INTEGER,
duration_secs DOUBLE PRECISION,
description TEXT,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS tags (
id UUID PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
parent_id UUID REFERENCES tags(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL
id UUID PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
parent_id UUID REFERENCES tags (id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_name_parent ON tags(name, COALESCE(parent_id, '00000000-0000-0000-0000-000000000000'));
CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_name_parent ON tags (
name,
COALESCE(parent_id, '00000000-0000-0000-0000-000000000000')
);
CREATE TABLE IF NOT EXISTS media_tags (
media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (media_id, tag_id)
media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE,
tag_id UUID NOT NULL REFERENCES tags (id) ON DELETE CASCADE,
PRIMARY KEY (media_id, tag_id)
);
CREATE TABLE IF NOT EXISTS collections (
id UUID PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
description TEXT,
kind TEXT NOT NULL,
filter_query TEXT,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
id UUID PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
description TEXT,
kind TEXT NOT NULL,
filter_query TEXT,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS collection_members (
collection_id UUID NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
position INTEGER NOT NULL DEFAULT 0,
added_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (collection_id, media_id)
collection_id UUID NOT NULL REFERENCES collections (id) ON DELETE CASCADE,
media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE,
position INTEGER NOT NULL DEFAULT 0,
added_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (collection_id, media_id)
);
CREATE TABLE IF NOT EXISTS audit_log (
id UUID PRIMARY KEY NOT NULL,
media_id UUID REFERENCES media_items(id) ON DELETE SET NULL,
action TEXT NOT NULL,
details TEXT,
timestamp TIMESTAMPTZ NOT NULL
id UUID PRIMARY KEY NOT NULL,
media_id UUID REFERENCES media_items (id) ON DELETE SET NULL,
action TEXT NOT NULL,
details TEXT,
timestamp TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS custom_fields (
media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
field_name TEXT NOT NULL,
field_type TEXT NOT NULL,
field_value TEXT NOT NULL,
PRIMARY KEY (media_id, field_name)
media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE,
field_name TEXT NOT NULL,
field_type TEXT NOT NULL,
field_value TEXT NOT NULL,
PRIMARY KEY (media_id, field_name)
);

View file

@ -1,11 +1,12 @@
ALTER TABLE media_items ADD COLUMN IF NOT EXISTS search_vector tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english', COALESCE(title, '')), 'A') ||
setweight(to_tsvector('english', COALESCE(artist, '')), 'B') ||
setweight(to_tsvector('english', COALESCE(album, '')), 'B') ||
setweight(to_tsvector('english', COALESCE(genre, '')), 'C') ||
setweight(to_tsvector('english', COALESCE(description, '')), 'C') ||
setweight(to_tsvector('english', COALESCE(file_name, '')), 'D')
) STORED;
ALTER TABLE media_items
ADD COLUMN IF NOT EXISTS search_vector tsvector GENERATED ALWAYS AS (
setweight(to_tsvector('english', COALESCE(title, '')), 'A') || setweight(to_tsvector('english', COALESCE(artist, '')), 'B') || setweight(to_tsvector('english', COALESCE(album, '')), 'B') || setweight(to_tsvector('english', COALESCE(genre, '')), 'C') || setweight(
to_tsvector('english', COALESCE(description, '')),
'C'
) || setweight(
to_tsvector('english', COALESCE(file_name, '')),
'D'
)
) STORED;
CREATE INDEX IF NOT EXISTS idx_media_search ON media_items USING GIN(search_vector);
CREATE INDEX IF NOT EXISTS idx_media_search ON media_items USING GIN (search_vector);

View file

@ -1,8 +1,15 @@
CREATE INDEX IF NOT EXISTS idx_audit_media_id ON audit_log(media_id);
CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp);
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log(action);
CREATE INDEX IF NOT EXISTS idx_media_content_hash ON media_items(content_hash);
CREATE INDEX IF NOT EXISTS idx_media_media_type ON media_items(media_type);
CREATE INDEX IF NOT EXISTS idx_media_created_at ON media_items(created_at);
CREATE INDEX IF NOT EXISTS idx_media_title_trgm ON media_items USING GIN(title gin_trgm_ops);
CREATE INDEX IF NOT EXISTS idx_media_artist_trgm ON media_items USING GIN(artist gin_trgm_ops);
CREATE INDEX IF NOT EXISTS idx_audit_media_id ON audit_log (media_id);
CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log (timestamp);
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log (action);
CREATE INDEX IF NOT EXISTS idx_media_content_hash ON media_items (content_hash);
CREATE INDEX IF NOT EXISTS idx_media_media_type ON media_items (media_type);
CREATE INDEX IF NOT EXISTS idx_media_created_at ON media_items (created_at);
CREATE INDEX IF NOT EXISTS idx_media_title_trgm ON media_items USING GIN (title gin_trgm_ops);
CREATE INDEX IF NOT EXISTS idx_media_artist_trgm ON media_items USING GIN (artist gin_trgm_ops);

View file

@ -1 +1,2 @@
ALTER TABLE media_items ADD COLUMN thumbnail_path TEXT;
ALTER TABLE media_items
ADD COLUMN thumbnail_path TEXT;

View file

@ -1,12 +1,15 @@
-- Integrity tracking columns
ALTER TABLE media_items ADD COLUMN last_verified_at TIMESTAMPTZ;
ALTER TABLE media_items ADD COLUMN integrity_status TEXT DEFAULT 'unverified';
ALTER TABLE media_items
ADD COLUMN last_verified_at TIMESTAMPTZ;
ALTER TABLE media_items
ADD COLUMN integrity_status TEXT DEFAULT 'unverified';
-- Saved searches
CREATE TABLE IF NOT EXISTS saved_searches (
id UUID PRIMARY KEY,
name TEXT NOT NULL,
query TEXT NOT NULL,
sort_order TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
id UUID PRIMARY KEY,
name TEXT NOT NULL,
query TEXT NOT NULL,
sort_order TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

View file

@ -1,15 +1,16 @@
-- Plugin registry table
CREATE TABLE plugin_registry (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
version TEXT NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
config_json TEXT,
manifest_json TEXT,
installed_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
version TEXT NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
config_json TEXT,
manifest_json TEXT,
installed_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
);
-- Index for quick lookups
CREATE INDEX idx_plugin_registry_enabled ON plugin_registry(enabled);
CREATE INDEX idx_plugin_registry_name ON plugin_registry(name);
CREATE INDEX idx_plugin_registry_enabled ON plugin_registry (enabled);
CREATE INDEX idx_plugin_registry_name ON plugin_registry (name);

View file

@ -1,35 +1,37 @@
-- Users table
CREATE TABLE users (
id UUID PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role JSONB NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
id UUID PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role JSONB NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
);
-- User profiles table
CREATE TABLE user_profiles (
user_id UUID PRIMARY KEY,
avatar_path TEXT,
bio TEXT,
preferences_json JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
user_id UUID PRIMARY KEY,
avatar_path TEXT,
bio TEXT,
preferences_json JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id)
);
-- User library access table
CREATE TABLE user_libraries (
user_id UUID NOT NULL,
root_path TEXT NOT NULL,
permission JSONB NOT NULL,
granted_at TIMESTAMP WITH TIME ZONE NOT NULL,
PRIMARY KEY (user_id, root_path),
FOREIGN KEY (user_id) REFERENCES users(id)
user_id UUID NOT NULL,
root_path TEXT NOT NULL,
permission JSONB NOT NULL,
granted_at TIMESTAMP WITH TIME ZONE NOT NULL,
PRIMARY KEY (user_id, root_path),
FOREIGN KEY (user_id) REFERENCES users (id)
);
-- Indexes for efficient lookups
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_user_libraries_user_id ON user_libraries(user_id);
CREATE INDEX idx_user_libraries_root_path ON user_libraries(root_path);
CREATE INDEX idx_users_username ON users (username);
CREATE INDEX idx_user_libraries_user_id ON user_libraries (user_id);
CREATE INDEX idx_user_libraries_root_path ON user_libraries (root_path);

View file

@ -1,131 +1,136 @@
-- Ratings
CREATE TABLE IF NOT EXISTS ratings (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
stars INTEGER NOT NULL CHECK (stars >= 1 AND stars <= 5),
review_text TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(user_id, media_id)
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE,
stars INTEGER NOT NULL CHECK (
stars >= 1
AND stars <= 5
),
review_text TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (user_id, media_id)
);
-- Comments
CREATE TABLE IF NOT EXISTS comments (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
parent_comment_id UUID REFERENCES comments(id) ON DELETE CASCADE,
text TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE,
parent_comment_id UUID REFERENCES comments (id) ON DELETE CASCADE,
text TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Favorites
CREATE TABLE IF NOT EXISTS favorites (
user_id UUID NOT NULL,
media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id, media_id)
user_id UUID NOT NULL,
media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id, media_id)
);
-- Share links
CREATE TABLE IF NOT EXISTS share_links (
id UUID PRIMARY KEY,
media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
created_by UUID NOT NULL,
token TEXT NOT NULL UNIQUE,
password_hash TEXT,
expires_at TIMESTAMPTZ,
view_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
id UUID PRIMARY KEY,
media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE,
created_by UUID NOT NULL,
token TEXT NOT NULL UNIQUE,
password_hash TEXT,
expires_at TIMESTAMPTZ,
view_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Playlists
CREATE TABLE IF NOT EXISTS playlists (
id UUID PRIMARY KEY,
owner_id UUID NOT NULL,
name TEXT NOT NULL,
description TEXT,
is_public BOOLEAN NOT NULL DEFAULT FALSE,
is_smart BOOLEAN NOT NULL DEFAULT FALSE,
filter_query TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
id UUID PRIMARY KEY,
owner_id UUID NOT NULL,
name TEXT NOT NULL,
description TEXT,
is_public BOOLEAN NOT NULL DEFAULT FALSE,
is_smart BOOLEAN NOT NULL DEFAULT FALSE,
filter_query TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Playlist items
CREATE TABLE IF NOT EXISTS playlist_items (
playlist_id UUID NOT NULL REFERENCES playlists(id) ON DELETE CASCADE,
media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
position INTEGER NOT NULL DEFAULT 0,
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (playlist_id, media_id)
playlist_id UUID NOT NULL REFERENCES playlists (id) ON DELETE CASCADE,
media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE,
position INTEGER NOT NULL DEFAULT 0,
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (playlist_id, media_id)
);
-- Usage events
CREATE TABLE IF NOT EXISTS usage_events (
id UUID PRIMARY KEY,
media_id UUID REFERENCES media_items(id) ON DELETE SET NULL,
user_id UUID,
event_type TEXT NOT NULL,
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
duration_secs DOUBLE PRECISION,
context_json JSONB
id UUID PRIMARY KEY,
media_id UUID REFERENCES media_items (id) ON DELETE SET NULL,
user_id UUID,
event_type TEXT NOT NULL,
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
duration_secs DOUBLE PRECISION,
context_json JSONB
);
CREATE INDEX IF NOT EXISTS idx_usage_events_media ON usage_events(media_id);
CREATE INDEX IF NOT EXISTS idx_usage_events_user ON usage_events(user_id);
CREATE INDEX IF NOT EXISTS idx_usage_events_timestamp ON usage_events(timestamp);
CREATE INDEX IF NOT EXISTS idx_usage_events_media ON usage_events (media_id);
CREATE INDEX IF NOT EXISTS idx_usage_events_user ON usage_events (user_id);
CREATE INDEX IF NOT EXISTS idx_usage_events_timestamp ON usage_events (timestamp);
-- Watch history / progress
CREATE TABLE IF NOT EXISTS watch_history (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
progress_secs DOUBLE PRECISION NOT NULL DEFAULT 0,
last_watched TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(user_id, media_id)
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE,
progress_secs DOUBLE PRECISION NOT NULL DEFAULT 0,
last_watched TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (user_id, media_id)
);
-- Subtitles
CREATE TABLE IF NOT EXISTS subtitles (
id UUID PRIMARY KEY,
media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
language TEXT,
format TEXT NOT NULL,
file_path TEXT,
is_embedded BOOLEAN NOT NULL DEFAULT FALSE,
track_index INTEGER,
offset_ms INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
id UUID PRIMARY KEY,
media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE,
language TEXT,
format TEXT NOT NULL,
file_path TEXT,
is_embedded BOOLEAN NOT NULL DEFAULT FALSE,
track_index INTEGER,
offset_ms INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_subtitles_media ON subtitles(media_id);
CREATE INDEX IF NOT EXISTS idx_subtitles_media ON subtitles (media_id);
-- External metadata (enrichment)
CREATE TABLE IF NOT EXISTS external_metadata (
id UUID PRIMARY KEY,
media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
source TEXT NOT NULL,
external_id TEXT,
metadata_json JSONB NOT NULL DEFAULT '{}',
confidence DOUBLE PRECISION NOT NULL DEFAULT 0.0,
last_updated TIMESTAMPTZ NOT NULL DEFAULT NOW()
id UUID PRIMARY KEY,
media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE,
source TEXT NOT NULL,
external_id TEXT,
metadata_json JSONB NOT NULL DEFAULT '{}',
confidence DOUBLE PRECISION NOT NULL DEFAULT 0.0,
last_updated TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_external_metadata_media ON external_metadata(media_id);
CREATE INDEX IF NOT EXISTS idx_external_metadata_media ON external_metadata (media_id);
-- Transcode sessions
CREATE TABLE IF NOT EXISTS transcode_sessions (
id UUID PRIMARY KEY,
media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
user_id UUID,
profile TEXT NOT NULL,
cache_path TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
progress DOUBLE PRECISION NOT NULL DEFAULT 0.0,
error_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ
id UUID PRIMARY KEY,
media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE,
user_id UUID,
profile TEXT NOT NULL,
cache_path TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
progress DOUBLE PRECISION NOT NULL DEFAULT 0.0,
error_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_transcode_sessions_media ON transcode_sessions(media_id);
CREATE INDEX IF NOT EXISTS idx_transcode_sessions_media ON transcode_sessions (media_id);

View file

@ -1,19 +1,26 @@
-- Drop redundant indexes (already covered by UNIQUE constraints)
DROP INDEX IF EXISTS idx_users_username;
DROP INDEX IF EXISTS idx_user_libraries_user_id;
-- Add missing indexes for comments table
CREATE INDEX IF NOT EXISTS idx_comments_media ON comments(media_id);
CREATE INDEX IF NOT EXISTS idx_comments_parent ON comments(parent_comment_id);
CREATE INDEX IF NOT EXISTS idx_comments_media ON comments (media_id);
CREATE INDEX IF NOT EXISTS idx_comments_parent ON comments (parent_comment_id);
-- Remove duplicates before adding unique constraint
DELETE FROM external_metadata e1
WHERE EXISTS (
SELECT 1 FROM external_metadata e2
WHERE e1.media_id = e2.media_id
AND e1.source = e2.source
AND e1.ctid < e2.ctid
);
WHERE
EXISTS (
SELECT
1
FROM
external_metadata e2
WHERE
e1.media_id = e2.media_id
AND e1.source = e2.source
AND e1.ctid < e2.ctid
);
-- Add unique constraint for external_metadata (idempotent)
DO $$