treewide: move migration logic into pinakes-migrations crate
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I98b8ed2eee464ecfd42f492dec49adeb6a6a6964
This commit is contained in:
parent
33d4ffe2bc
commit
9f9aa80265
43 changed files with 76 additions and 31 deletions
|
|
@ -29,7 +29,7 @@ deadpool-postgres = { workspace = true }
|
|||
postgres-types = { workspace = true }
|
||||
postgres-native-tls = { workspace = true }
|
||||
native-tls = { workspace = true }
|
||||
refinery = { workspace = true }
|
||||
pinakes-migrations = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
notify = { workspace = true }
|
||||
winnow = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -1,28 +1,17 @@
|
|||
use crate::error::{PinakesError, Result};
|
||||
|
||||
mod sqlite_migrations {
|
||||
use refinery::embed_migrations;
|
||||
embed_migrations!("../../migrations/sqlite");
|
||||
}
|
||||
|
||||
mod postgres_migrations {
|
||||
use refinery::embed_migrations;
|
||||
embed_migrations!("../../migrations/postgres");
|
||||
}
|
||||
|
||||
pub fn run_sqlite_migrations(conn: &mut rusqlite::Connection) -> Result<()> {
|
||||
sqlite_migrations::migrations::runner()
|
||||
.run(conn)
|
||||
.map_err(|e| PinakesError::Migration(e.to_string()))?;
|
||||
Ok(())
|
||||
pinakes_migrations::sqlite_migrations()
|
||||
.to_latest(conn)
|
||||
.map_err(|e| PinakesError::Migration(e.to_string()))
|
||||
}
|
||||
|
||||
pub async fn run_postgres_migrations(
|
||||
client: &mut tokio_postgres::Client,
|
||||
) -> Result<()> {
|
||||
postgres_migrations::migrations::runner()
|
||||
pinakes_migrations::postgres_runner()
|
||||
.run_async(client)
|
||||
.await
|
||||
.map_err(|e| PinakesError::Migration(e.to_string()))?;
|
||||
Ok(())
|
||||
.map(|_| ())
|
||||
.map_err(|e| PinakesError::Migration(e.to_string()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1082,8 +1082,8 @@ impl StorageBackend for SqliteBackend {
|
|||
.map_err(|e| PinakesError::Database(e.to_string()))?;
|
||||
let count: u64 =
|
||||
db.query_row("SELECT COUNT(*) FROM media_items", [], |row| {
|
||||
row.get(0)
|
||||
})?;
|
||||
row.get::<_, i64>(0)
|
||||
})?.cast_unsigned();
|
||||
db.execute("DELETE FROM media_items", [])?;
|
||||
count
|
||||
};
|
||||
|
|
@ -2441,12 +2441,12 @@ impl StorageBackend for SqliteBackend {
|
|||
.map_err(|e| PinakesError::Database(e.to_string()))?;
|
||||
|
||||
let total_media: u64 =
|
||||
db.query_row("SELECT COUNT(*) FROM media_items", [], |r| r.get(0))?;
|
||||
db.query_row("SELECT COUNT(*) FROM media_items", [], |r| r.get::<_, i64>(0))?.cast_unsigned();
|
||||
let total_size: u64 = db.query_row(
|
||||
"SELECT COALESCE(SUM(file_size), 0) FROM media_items",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)?;
|
||||
|r| r.get::<_, i64>(0),
|
||||
)?.cast_unsigned();
|
||||
let avg_size: u64 = total_size.checked_div(total_media).unwrap_or(0);
|
||||
|
||||
// Media count by type
|
||||
|
|
@ -2455,7 +2455,7 @@ impl StorageBackend for SqliteBackend {
|
|||
ORDER BY COUNT(*) DESC",
|
||||
)?;
|
||||
let media_by_type: Vec<(String, u64)> = stmt
|
||||
.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, u64>(1)?)))?
|
||||
.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?.cast_unsigned())))?
|
||||
.filter_map(std::result::Result::ok)
|
||||
.collect();
|
||||
|
||||
|
|
@ -2465,7 +2465,7 @@ impl StorageBackend for SqliteBackend {
|
|||
GROUP BY media_type ORDER BY SUM(file_size) DESC",
|
||||
)?;
|
||||
let storage_by_type: Vec<(String, u64)> = stmt
|
||||
.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, u64>(1)?)))?
|
||||
.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?.cast_unsigned())))?
|
||||
.filter_map(std::result::Result::ok)
|
||||
.collect();
|
||||
|
||||
|
|
@ -2491,7 +2491,7 @@ impl StorageBackend for SqliteBackend {
|
|||
mt.tag_id = t.id GROUP BY t.id ORDER BY cnt DESC LIMIT 10",
|
||||
)?;
|
||||
let top_tags: Vec<(String, u64)> = stmt
|
||||
.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, u64>(1)?)))?
|
||||
.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?.cast_unsigned())))?
|
||||
.filter_map(std::result::Result::ok)
|
||||
.collect();
|
||||
|
||||
|
|
@ -2502,22 +2502,22 @@ impl StorageBackend for SqliteBackend {
|
|||
DESC LIMIT 10",
|
||||
)?;
|
||||
let top_collections: Vec<(String, u64)> = stmt
|
||||
.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, u64>(1)?)))?
|
||||
.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?.cast_unsigned())))?
|
||||
.filter_map(std::result::Result::ok)
|
||||
.collect();
|
||||
|
||||
let total_tags: u64 =
|
||||
db.query_row("SELECT COUNT(*) FROM tags", [], |r| r.get(0))?;
|
||||
db.query_row("SELECT COUNT(*) FROM tags", [], |r| r.get::<_, i64>(0))?.cast_unsigned();
|
||||
let total_collections: u64 =
|
||||
db.query_row("SELECT COUNT(*) FROM collections", [], |r| r.get(0))?;
|
||||
db.query_row("SELECT COUNT(*) FROM collections", [], |r| r.get::<_, i64>(0))?.cast_unsigned();
|
||||
|
||||
// Duplicates: count of hashes that appear more than once
|
||||
let total_duplicates: u64 = db.query_row(
|
||||
"SELECT COUNT(*) FROM (SELECT content_hash FROM media_items GROUP BY \
|
||||
content_hash HAVING COUNT(*) > 1)",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)?;
|
||||
|r| r.get::<_, i64>(0),
|
||||
)?.cast_unsigned();
|
||||
|
||||
Ok(super::LibraryStatistics {
|
||||
total_media,
|
||||
|
|
|
|||
14
crates/pinakes-migrations/Cargo.toml
Normal file
14
crates/pinakes-migrations/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "pinakes-migrations"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
rusqlite = { workspace = true }
|
||||
tokio-postgres = { workspace = true }
|
||||
rusqlite_migration = { workspace = true }
|
||||
refinery = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
@ -0,0 +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;
|
||||
|
||||
-- Create index for quick mtime lookups
|
||||
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
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_scan_history_directory ON scan_history (directory);
|
||||
|
|
@ -0,0 +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
|
||||
);
|
||||
|
||||
-- Index for efficient cleanup of expired sessions
|
||||
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);
|
||||
|
|
@ -0,0 +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()
|
||||
);
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
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 $$
|
||||
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 ();
|
||||
|
|
@ -0,0 +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 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;
|
||||
|
|
@ -0,0 +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;
|
||||
|
||||
-- Index for perceptual hash lookups
|
||||
CREATE INDEX idx_media_phash ON media_items (perceptual_hash)
|
||||
WHERE
|
||||
perceptual_hash IS NOT NULL;
|
||||
|
|
@ -0,0 +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';
|
||||
|
||||
-- Original filename for managed uploads (preserved separately from file_name which may be normalized)
|
||||
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;
|
||||
|
||||
-- Storage key for looking up the blob (usually same as content_hash for deduplication)
|
||||
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
|
||||
);
|
||||
|
||||
-- Index for finding managed media items
|
||||
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);
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
-- 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
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
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,
|
||||
size BIGINT NOT NULL,
|
||||
hash TEXT NOT NULL,
|
||||
received_at TIMESTAMPTZ NOT NULL,
|
||||
PRIMARY KEY (upload_id, chunk_index)
|
||||
);
|
||||
|
||||
-- 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
|
||||
);
|
||||
|
||||
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;
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
-- 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
|
||||
)
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
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 $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'share_links') THEN
|
||||
INSERT INTO shares (
|
||||
id, target_type, target_id, owner_id, recipient_type,
|
||||
public_token, public_password_hash, perm_view, perm_download,
|
||||
access_count, expires_at, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
id, 'media', media_id, created_by, 'public_link',
|
||||
token, password_hash, TRUE, TRUE,
|
||||
view_count, expires_at, created_at, created_at
|
||||
FROM share_links
|
||||
ON CONFLICT DO NOTHING;
|
||||
END IF;
|
||||
END $$;
|
||||
|
|
@ -0,0 +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;
|
||||
|
||||
-- Index for efficient trash queries
|
||||
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;
|
||||
|
|
@ -0,0 +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
|
||||
);
|
||||
|
||||
-- Index for efficient outgoing link queries (what does this note link to?)
|
||||
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);
|
||||
|
||||
-- Index for path-based resolution (finding unresolved links)
|
||||
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);
|
||||
|
||||
-- Track when links were last extracted from a media item
|
||||
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);
|
||||
|
|
@ -0,0 +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 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
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
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)
|
||||
);
|
||||
|
|
@ -0,0 +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;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_media_search ON media_items USING GIN (search_vector);
|
||||
|
|
@ -0,0 +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);
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE media_items
|
||||
ADD COLUMN thumbnail_path TEXT;
|
||||
|
|
@ -0,0 +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';
|
||||
|
||||
-- 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()
|
||||
);
|
||||
|
|
@ -0,0 +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
|
||||
);
|
||||
|
||||
-- Index for quick lookups
|
||||
CREATE INDEX idx_plugin_registry_enabled ON plugin_registry (enabled);
|
||||
|
||||
CREATE INDEX idx_plugin_registry_name ON plugin_registry (name);
|
||||
|
|
@ -0,0 +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
|
||||
);
|
||||
|
||||
-- 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);
|
||||
|
|
@ -0,0 +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)
|
||||
);
|
||||
|
||||
-- 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);
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
-- 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 $$;
|
||||
|
|
@ -0,0 +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 INTEGER;
|
||||
|
||||
-- Create index for quick mtime lookups
|
||||
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 TEXT PRIMARY KEY,
|
||||
directory TEXT NOT NULL UNIQUE,
|
||||
last_scan_at TEXT NOT NULL,
|
||||
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);
|
||||
|
|
@ -0,0 +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 TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
last_accessed TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Index for efficient cleanup of expired sessions
|
||||
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);
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
-- V12: Book Management Schema
|
||||
-- Adds comprehensive book metadata tracking, authors, and identifiers
|
||||
-- Book metadata (supplements media_items for EPUB/PDF/MOBI)
|
||||
CREATE TABLE book_metadata (
|
||||
media_id TEXT 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 TEXT, -- ISO 8601 date string
|
||||
series_name TEXT,
|
||||
series_index REAL, -- Supports 1.5, etc.
|
||||
format TEXT, -- 'epub', 'pdf', 'mobi', 'azw3'
|
||||
created_at TEXT NOT NULL DEFAULT (datetime ('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime ('now'))
|
||||
) STRICT;
|
||||
|
||||
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 TEXT 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)
|
||||
) STRICT;
|
||||
|
||||
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 TEXT 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)
|
||||
) STRICT;
|
||||
|
||||
CREATE INDEX idx_book_identifiers ON book_identifiers (identifier_type, identifier_value);
|
||||
|
||||
-- Trigger to update updated_at on book_metadata changes
|
||||
CREATE TRIGGER update_book_metadata_timestamp
|
||||
AFTER
|
||||
UPDATE ON book_metadata FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE book_metadata
|
||||
SET
|
||||
updated_at = datetime ('now')
|
||||
WHERE
|
||||
media_id = NEW.media_id;
|
||||
|
||||
END;
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
-- V13: Enhanced photo metadata support
|
||||
-- Add photo-specific fields to media_items table
|
||||
ALTER TABLE media_items
|
||||
ADD COLUMN date_taken TIMESTAMP;
|
||||
|
||||
ALTER TABLE media_items
|
||||
ADD COLUMN latitude REAL;
|
||||
|
||||
ALTER TABLE media_items
|
||||
ADD COLUMN longitude REAL;
|
||||
|
||||
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;
|
||||
|
|
@ -0,0 +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;
|
||||
|
||||
-- Index for perceptual hash lookups
|
||||
CREATE INDEX idx_media_phash ON media_items (perceptual_hash)
|
||||
WHERE
|
||||
perceptual_hash IS NOT NULL;
|
||||
|
|
@ -0,0 +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';
|
||||
|
||||
-- Original filename for managed uploads (preserved separately from file_name which may be normalized)
|
||||
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 TEXT;
|
||||
|
||||
-- Storage key for looking up the blob (usually same as content_hash for deduplication)
|
||||
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 INTEGER NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
reference_count INTEGER NOT NULL DEFAULT 1,
|
||||
stored_at TEXT NOT NULL,
|
||||
last_verified TEXT
|
||||
);
|
||||
|
||||
-- Index for finding managed media items
|
||||
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);
|
||||
129
crates/pinakes-migrations/migrations/sqlite/V16__sync_system.sql
Normal file
129
crates/pinakes-migrations/migrations/sqlite/V16__sync_system.sql
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
-- 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,
|
||||
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 TEXT,
|
||||
last_seen_at TEXT NOT NULL,
|
||||
sync_cursor INTEGER DEFAULT 0,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
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 INTEGER NOT NULL UNIQUE,
|
||||
change_type TEXT NOT NULL,
|
||||
media_id TEXT,
|
||||
path TEXT NOT NULL,
|
||||
content_hash TEXT,
|
||||
file_size INTEGER,
|
||||
metadata_json TEXT,
|
||||
changed_by_device TEXT,
|
||||
timestamp TEXT NOT NULL,
|
||||
FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (changed_by_device) REFERENCES sync_devices (id) ON DELETE SET 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);
|
||||
|
||||
-- Sequence counter for sync log
|
||||
CREATE TABLE sync_sequence (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
current_value INTEGER NOT NULL DEFAULT 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,
|
||||
path TEXT NOT NULL,
|
||||
local_hash TEXT,
|
||||
server_hash TEXT,
|
||||
local_mtime INTEGER,
|
||||
server_mtime INTEGER,
|
||||
sync_status TEXT NOT NULL,
|
||||
last_synced_at TEXT,
|
||||
conflict_info_json TEXT,
|
||||
PRIMARY KEY (device_id, path),
|
||||
FOREIGN KEY (device_id) REFERENCES sync_devices (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
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,
|
||||
target_path TEXT NOT NULL,
|
||||
expected_hash TEXT NOT NULL,
|
||||
expected_size INTEGER NOT NULL,
|
||||
chunk_size INTEGER NOT NULL,
|
||||
chunk_count INTEGER NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
last_activity TEXT NOT NULL,
|
||||
FOREIGN KEY (device_id) REFERENCES sync_devices (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
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,
|
||||
chunk_index INTEGER NOT NULL,
|
||||
offset
|
||||
INTEGER NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
hash TEXT NOT NULL,
|
||||
received_at TEXT NOT NULL,
|
||||
PRIMARY KEY (upload_id, chunk_index),
|
||||
FOREIGN KEY (upload_id) REFERENCES upload_sessions (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Sync conflicts
|
||||
CREATE TABLE sync_conflicts (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
device_id TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
local_hash TEXT NOT NULL,
|
||||
local_mtime INTEGER NOT NULL,
|
||||
server_hash TEXT NOT NULL,
|
||||
server_mtime INTEGER NOT NULL,
|
||||
detected_at TEXT NOT NULL,
|
||||
resolved_at TEXT,
|
||||
resolution TEXT,
|
||||
FOREIGN KEY (device_id) REFERENCES sync_devices (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_sync_conflicts_device ON sync_conflicts (device_id);
|
||||
|
||||
CREATE INDEX idx_sync_conflicts_unresolved ON sync_conflicts (device_id, resolved_at)
|
||||
WHERE
|
||||
resolved_at IS NULL;
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
-- 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,
|
||||
recipient_type TEXT NOT NULL CHECK (
|
||||
recipient_type IN ('public_link', 'user', 'group', 'federated')
|
||||
),
|
||||
recipient_user_id TEXT,
|
||||
recipient_group_id TEXT,
|
||||
recipient_federated_handle TEXT,
|
||||
recipient_federated_server TEXT,
|
||||
public_token TEXT UNIQUE,
|
||||
public_password_hash TEXT,
|
||||
perm_view INTEGER NOT NULL DEFAULT 1,
|
||||
perm_download INTEGER NOT NULL DEFAULT 0,
|
||||
perm_edit INTEGER NOT NULL DEFAULT 0,
|
||||
perm_delete INTEGER NOT NULL DEFAULT 0,
|
||||
perm_reshare INTEGER NOT NULL DEFAULT 0,
|
||||
perm_add INTEGER NOT NULL DEFAULT 0,
|
||||
note TEXT,
|
||||
expires_at TEXT,
|
||||
access_count INTEGER NOT NULL DEFAULT 0,
|
||||
last_accessed TEXT,
|
||||
inherit_to_children INTEGER NOT NULL DEFAULT 1,
|
||||
parent_share_id TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (owner_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (recipient_user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (parent_share_id) REFERENCES shares (id) ON DELETE CASCADE,
|
||||
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);
|
||||
|
||||
-- Share activity log
|
||||
CREATE TABLE share_activity (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
share_id TEXT NOT NULL,
|
||||
actor_id TEXT,
|
||||
actor_ip TEXT,
|
||||
action TEXT NOT NULL,
|
||||
details TEXT,
|
||||
timestamp TEXT NOT NULL,
|
||||
FOREIGN KEY (share_id) REFERENCES shares (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (actor_id) REFERENCES users (id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
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,
|
||||
share_id TEXT NOT NULL,
|
||||
notification_type TEXT NOT NULL,
|
||||
is_read INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (share_id) REFERENCES shares (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_share_notifications_user ON share_notifications (user_id);
|
||||
|
||||
CREATE INDEX idx_share_notifications_unread ON share_notifications (user_id, is_read)
|
||||
WHERE
|
||||
is_read = 0;
|
||||
|
||||
-- Migrate existing share_links to new shares table (if share_links exists)
|
||||
INSERT
|
||||
OR IGNORE INTO shares (
|
||||
id,
|
||||
target_type,
|
||||
target_id,
|
||||
owner_id,
|
||||
recipient_type,
|
||||
public_token,
|
||||
public_password_hash,
|
||||
perm_view,
|
||||
perm_download,
|
||||
access_count,
|
||||
expires_at,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
'media',
|
||||
media_id,
|
||||
created_by,
|
||||
'public_link',
|
||||
token,
|
||||
password_hash,
|
||||
1,
|
||||
1,
|
||||
view_count,
|
||||
expires_at,
|
||||
created_at,
|
||||
created_at
|
||||
FROM
|
||||
share_links
|
||||
WHERE
|
||||
EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
sqlite_master
|
||||
WHERE
|
||||
type = 'table'
|
||||
AND name = 'share_links'
|
||||
);
|
||||
|
|
@ -0,0 +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 TEXT;
|
||||
|
||||
-- Index for efficient trash queries
|
||||
CREATE INDEX idx_media_deleted_at ON media_items (deleted_at);
|
||||
|
||||
-- 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;
|
||||
|
|
@ -0,0 +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 TEXT 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);
|
||||
|
||||
-- Index for efficient backlink queries (what links to this note?)
|
||||
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);
|
||||
|
||||
-- Index for link type filtering
|
||||
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 TEXT;
|
||||
|
||||
-- Index for finding media items that need link extraction
|
||||
CREATE INDEX idx_media_links_extracted ON media_items (links_extracted_at);
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
CREATE TABLE IF NOT EXISTS root_dirs (path TEXT PRIMARY KEY NOT NULL);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS media_items (
|
||||
id TEXT 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 INTEGER NOT NULL,
|
||||
title TEXT,
|
||||
artist TEXT,
|
||||
album TEXT,
|
||||
genre TEXT,
|
||||
year INTEGER,
|
||||
duration_secs REAL,
|
||||
description TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tags (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
parent_id TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY (parent_id) REFERENCES tags (id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_name_parent ON tags (name, parent_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS media_tags (
|
||||
media_id TEXT NOT NULL,
|
||||
tag_id TEXT NOT NULL,
|
||||
PRIMARY KEY (media_id, tag_id),
|
||||
FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS collections (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
kind TEXT NOT NULL,
|
||||
filter_query TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS collection_members (
|
||||
collection_id TEXT NOT NULL,
|
||||
media_id TEXT NOT NULL,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
added_at TEXT NOT NULL,
|
||||
PRIMARY KEY (collection_id, media_id),
|
||||
FOREIGN KEY (collection_id) REFERENCES collections (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
media_id TEXT,
|
||||
action TEXT NOT NULL,
|
||||
details TEXT,
|
||||
timestamp TEXT NOT NULL,
|
||||
FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS custom_fields (
|
||||
media_id TEXT NOT NULL,
|
||||
field_name TEXT NOT NULL,
|
||||
field_type TEXT NOT NULL,
|
||||
field_value TEXT NOT NULL,
|
||||
PRIMARY KEY (media_id, field_name),
|
||||
FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE
|
||||
);
|
||||
114
crates/pinakes-migrations/migrations/sqlite/V2__fts5_indexes.sql
Normal file
114
crates/pinakes-migrations/migrations/sqlite/V2__fts5_indexes.sql
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
CREATE VIRTUAL TABLE IF NOT EXISTS media_fts USING fts5 (
|
||||
title,
|
||||
artist,
|
||||
album,
|
||||
genre,
|
||||
description,
|
||||
file_name,
|
||||
content = 'media_items',
|
||||
content_rowid = 'rowid'
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS media_fts_insert
|
||||
AFTER INSERT ON media_items
|
||||
BEGIN
|
||||
INSERT INTO
|
||||
media_fts (
|
||||
rowid,
|
||||
title,
|
||||
artist,
|
||||
album,
|
||||
genre,
|
||||
description,
|
||||
file_name
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
new.rowid,
|
||||
new.title,
|
||||
new.artist,
|
||||
new.album,
|
||||
new.genre,
|
||||
new.description,
|
||||
new.file_name
|
||||
);
|
||||
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS media_fts_update
|
||||
AFTER
|
||||
UPDATE ON media_items
|
||||
BEGIN
|
||||
INSERT INTO
|
||||
media_fts (
|
||||
media_fts,
|
||||
rowid,
|
||||
title,
|
||||
artist,
|
||||
album,
|
||||
genre,
|
||||
description,
|
||||
file_name
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'delete',
|
||||
old.rowid,
|
||||
old.title,
|
||||
old.artist,
|
||||
old.album,
|
||||
old.genre,
|
||||
old.description,
|
||||
old.file_name
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
media_fts (
|
||||
rowid,
|
||||
title,
|
||||
artist,
|
||||
album,
|
||||
genre,
|
||||
description,
|
||||
file_name
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
new.rowid,
|
||||
new.title,
|
||||
new.artist,
|
||||
new.album,
|
||||
new.genre,
|
||||
new.description,
|
||||
new.file_name
|
||||
);
|
||||
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS media_fts_delete
|
||||
AFTER DELETE ON media_items
|
||||
BEGIN
|
||||
INSERT INTO
|
||||
media_fts (
|
||||
media_fts,
|
||||
rowid,
|
||||
title,
|
||||
artist,
|
||||
album,
|
||||
genre,
|
||||
description,
|
||||
file_name
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'delete',
|
||||
old.rowid,
|
||||
old.title,
|
||||
old.artist,
|
||||
old.album,
|
||||
old.genre,
|
||||
old.description,
|
||||
old.file_name
|
||||
);
|
||||
|
||||
END;
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
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);
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE media_items
|
||||
ADD COLUMN thumbnail_path TEXT;
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
-- Integrity tracking columns
|
||||
ALTER TABLE media_items
|
||||
ADD COLUMN last_verified_at TEXT;
|
||||
|
||||
ALTER TABLE media_items
|
||||
ADD COLUMN integrity_status TEXT DEFAULT 'unverified';
|
||||
|
||||
-- Saved searches
|
||||
CREATE TABLE IF NOT EXISTS saved_searches (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
query TEXT NOT NULL,
|
||||
sort_order TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
|
@ -0,0 +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 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);
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
-- 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);
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
-- 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);
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
-- 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);
|
||||
42
crates/pinakes-migrations/src/lib.rs
Normal file
42
crates/pinakes-migrations/src/lib.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
use rusqlite_migration::{M, Migrations};
|
||||
|
||||
mod postgres_migrations {
|
||||
use refinery::embed_migrations;
|
||||
embed_migrations!("migrations/postgres");
|
||||
}
|
||||
|
||||
pub fn sqlite_migrations() -> Migrations<'static> {
|
||||
Migrations::new(vec![
|
||||
M::up(include_str!("../migrations/sqlite/V1__initial_schema.sql")),
|
||||
M::up(include_str!("../migrations/sqlite/V2__fts5_indexes.sql")),
|
||||
M::up(include_str!("../migrations/sqlite/V3__audit_indexes.sql")),
|
||||
M::up(include_str!("../migrations/sqlite/V4__thumbnail_path.sql")),
|
||||
M::up(include_str!(
|
||||
"../migrations/sqlite/V5__integrity_and_saved_searches.sql"
|
||||
)),
|
||||
M::up(include_str!("../migrations/sqlite/V6__plugin_system.sql")),
|
||||
M::up(include_str!("../migrations/sqlite/V7__user_management.sql")),
|
||||
M::up(include_str!(
|
||||
"../migrations/sqlite/V8__media_server_features.sql"
|
||||
)),
|
||||
M::up(include_str!(
|
||||
"../migrations/sqlite/V9__fix_indexes_and_constraints.sql"
|
||||
)),
|
||||
M::up(include_str!("../migrations/sqlite/V10__incremental_scan.sql")),
|
||||
M::up(include_str!(
|
||||
"../migrations/sqlite/V11__session_persistence.sql"
|
||||
)),
|
||||
M::up(include_str!("../migrations/sqlite/V12__book_management.sql")),
|
||||
M::up(include_str!("../migrations/sqlite/V13__photo_metadata.sql")),
|
||||
M::up(include_str!("../migrations/sqlite/V14__perceptual_hash.sql")),
|
||||
M::up(include_str!("../migrations/sqlite/V15__managed_storage.sql")),
|
||||
M::up(include_str!("../migrations/sqlite/V16__sync_system.sql")),
|
||||
M::up(include_str!("../migrations/sqlite/V17__enhanced_sharing.sql")),
|
||||
M::up(include_str!("../migrations/sqlite/V18__file_management.sql")),
|
||||
M::up(include_str!("../migrations/sqlite/V19__markdown_links.sql")),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn postgres_runner() -> refinery::Runner {
|
||||
postgres_migrations::migrations::runner()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue