diff --git a/migrations/postgres/V6__plugin_system.sql b/migrations/postgres/V6__plugin_system.sql new file mode 100644 index 0000000..bd52a1a --- /dev/null +++ b/migrations/postgres/V6__plugin_system.sql @@ -0,0 +1,15 @@ +-- 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 +); + +-- Index for quick lookups +CREATE INDEX idx_plugin_registry_enabled ON plugin_registry(enabled); +CREATE INDEX idx_plugin_registry_name ON plugin_registry(name); diff --git a/migrations/postgres/V7__user_management.sql b/migrations/postgres/V7__user_management.sql new file mode 100644 index 0000000..a9eb12f --- /dev/null +++ b/migrations/postgres/V7__user_management.sql @@ -0,0 +1,35 @@ +-- 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 +); + +-- 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 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) +); + +-- 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); diff --git a/migrations/postgres/V8__media_server_features.sql b/migrations/postgres/V8__media_server_features.sql new file mode 100644 index 0000000..7d22838 --- /dev/null +++ b/migrations/postgres/V8__media_server_features.sql @@ -0,0 +1,131 @@ +-- 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) +); + +-- 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() +); + +-- 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) +); + +-- 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() +); + +-- 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() +); + +-- 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) +); + +-- 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 +); + +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) +); + +-- 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() +); + +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() +); + +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 +); + +CREATE INDEX IF NOT EXISTS idx_transcode_sessions_media ON transcode_sessions(media_id); diff --git a/migrations/postgres/V9__fix_indexes_and_constraints.sql b/migrations/postgres/V9__fix_indexes_and_constraints.sql new file mode 100644 index 0000000..a65fda3 --- /dev/null +++ b/migrations/postgres/V9__fix_indexes_and_constraints.sql @@ -0,0 +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); + +-- 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 +); + +-- Add unique constraint for external_metadata (idempotent) +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'uq_external_metadata_source' + ) THEN + ALTER TABLE external_metadata ADD CONSTRAINT uq_external_metadata_source UNIQUE(media_id, source); + END IF; +END $$; diff --git a/migrations/sqlite/V6__plugin_system.sql b/migrations/sqlite/V6__plugin_system.sql new file mode 100644 index 0000000..f4e7790 --- /dev/null +++ b/migrations/sqlite/V6__plugin_system.sql @@ -0,0 +1,15 @@ +-- 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 TEXT NOT NULL, + updated_at TEXT 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); diff --git a/migrations/sqlite/V7__user_management.sql b/migrations/sqlite/V7__user_management.sql new file mode 100644 index 0000000..6584f03 --- /dev/null +++ b/migrations/sqlite/V7__user_management.sql @@ -0,0 +1,35 @@ +-- Users table +CREATE TABLE users ( + id TEXT PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + role TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +-- User profiles table +CREATE TABLE user_profiles ( + user_id TEXT PRIMARY KEY, + avatar_path TEXT, + bio TEXT, + preferences_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) +); + +-- User library access table +CREATE TABLE user_libraries ( + user_id TEXT NOT NULL, + root_path TEXT NOT NULL, + permission TEXT NOT NULL, + granted_at TEXT 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); diff --git a/migrations/sqlite/V8__media_server_features.sql b/migrations/sqlite/V8__media_server_features.sql new file mode 100644 index 0000000..50040c3 --- /dev/null +++ b/migrations/sqlite/V8__media_server_features.sql @@ -0,0 +1,143 @@ +-- Ratings +CREATE TABLE IF NOT EXISTS ratings ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + media_id TEXT NOT NULL, + stars INTEGER NOT NULL CHECK (stars >= 1 AND stars <= 5), + review_text TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + UNIQUE(user_id, media_id), + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE +); + +-- Comments +CREATE TABLE IF NOT EXISTS comments ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + media_id TEXT NOT NULL, + parent_comment_id TEXT, + text TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE, + FOREIGN KEY (parent_comment_id) REFERENCES comments(id) ON DELETE CASCADE +); + +-- Favorites +CREATE TABLE IF NOT EXISTS favorites ( + user_id TEXT NOT NULL, + media_id TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + PRIMARY KEY (user_id, media_id), + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE +); + +-- Share links +CREATE TABLE IF NOT EXISTS share_links ( + id TEXT PRIMARY KEY, + media_id TEXT NOT NULL, + created_by TEXT NOT NULL, + token TEXT NOT NULL UNIQUE, + password_hash TEXT, + expires_at TEXT, + view_count INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE +); + +-- Playlists +CREATE TABLE IF NOT EXISTS playlists ( + id TEXT PRIMARY KEY, + owner_id TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT, + is_public INTEGER NOT NULL DEFAULT 0, + is_smart INTEGER NOT NULL DEFAULT 0, + filter_query TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +-- Playlist items +CREATE TABLE IF NOT EXISTS playlist_items ( + playlist_id TEXT NOT NULL, + media_id TEXT NOT NULL, + position INTEGER NOT NULL DEFAULT 0, + added_at TEXT NOT NULL DEFAULT (datetime('now')), + PRIMARY KEY (playlist_id, media_id), + FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE, + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE +); + +-- Usage events +CREATE TABLE IF NOT EXISTS usage_events ( + id TEXT PRIMARY KEY, + media_id TEXT, + user_id TEXT, + event_type TEXT NOT NULL, + timestamp TEXT NOT NULL DEFAULT (datetime('now')), + duration_secs REAL, + context_json TEXT, + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE SET NULL +); + +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 TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + media_id TEXT NOT NULL, + progress_secs REAL NOT NULL DEFAULT 0, + last_watched TEXT NOT NULL DEFAULT (datetime('now')), + UNIQUE(user_id, media_id), + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE +); + +-- Subtitles +CREATE TABLE IF NOT EXISTS subtitles ( + id TEXT PRIMARY KEY, + media_id TEXT NOT NULL, + language TEXT, + format TEXT NOT NULL, + file_path TEXT, + is_embedded INTEGER NOT NULL DEFAULT 0, + track_index INTEGER, + offset_ms INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_subtitles_media ON subtitles(media_id); + +-- External metadata (enrichment) +CREATE TABLE IF NOT EXISTS external_metadata ( + id TEXT PRIMARY KEY, + media_id TEXT NOT NULL, + source TEXT NOT NULL, + external_id TEXT, + metadata_json TEXT NOT NULL DEFAULT '{}', + confidence REAL NOT NULL DEFAULT 0.0, + last_updated TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_external_metadata_media ON external_metadata(media_id); + +-- Transcode sessions +CREATE TABLE IF NOT EXISTS transcode_sessions ( + id TEXT PRIMARY KEY, + media_id TEXT NOT NULL, + user_id TEXT, + profile TEXT NOT NULL, + cache_path TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + progress REAL NOT NULL DEFAULT 0.0, + error_message TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + expires_at TEXT, + FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_transcode_sessions_media ON transcode_sessions(media_id); diff --git a/migrations/sqlite/V9__fix_indexes_and_constraints.sql b/migrations/sqlite/V9__fix_indexes_and_constraints.sql new file mode 100644 index 0000000..432f35a --- /dev/null +++ b/migrations/sqlite/V9__fix_indexes_and_constraints.sql @@ -0,0 +1,18 @@ +-- 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); + +-- Remove duplicates before adding unique index (keep the first row) +DELETE FROM external_metadata +WHERE rowid NOT IN ( + SELECT MIN(rowid) + FROM external_metadata + GROUP BY media_id, source +); + +-- Add unique index for external_metadata to prevent duplicates +CREATE UNIQUE INDEX IF NOT EXISTS uq_external_metadata_source ON external_metadata(media_id, source);