const MIGRATION_001: &str = r#"
-- Jellyfin servers the user has connected to
CREATE TABLE IF NOT EXISTS servers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
url TEXT NOT NULL UNIQUE,
version TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
last_connected_at TEXT
);
-- User accounts on Jellyfin servers
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
server_id TEXT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
username TEXT NOT NULL,
access_token TEXT,
is_active INTEGER DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
last_login_at TEXT,
UNIQUE(server_id, username)
);
-- Libraries/views from Jellyfin
CREATE TABLE IF NOT EXISTS libraries (
id TEXT PRIMARY KEY,
server_id TEXT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
name TEXT NOT NULL,
collection_type TEXT,
image_tag TEXT,
sort_order INTEGER DEFAULT 0,
synced_at TEXT,
UNIQUE(server_id, id)
);
-- Media items (movies, shows, episodes, albums, songs, artists)
CREATE TABLE IF NOT EXISTS items (
id TEXT PRIMARY KEY,
server_id TEXT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
library_id TEXT REFERENCES libraries(id) ON DELETE SET NULL,
parent_id TEXT REFERENCES items(id) ON DELETE CASCADE,
-- Core metadata
name TEXT NOT NULL,
sort_name TEXT,
original_title TEXT,
item_type TEXT NOT NULL, -- Movie, Series, Episode, MusicAlbum, Audio, MusicArtist, etc.
-- Media info
overview TEXT,
tagline TEXT,
genres TEXT, -- JSON array
tags TEXT, -- JSON array
studios TEXT, -- JSON array
-- For episodes
series_id TEXT,
series_name TEXT,
season_id TEXT,
season_name TEXT,
index_number INTEGER, -- Episode number
parent_index_number INTEGER, -- Season number
-- For music
album_id TEXT,
album_name TEXT,
album_artist TEXT,
artists TEXT, -- JSON array
-- Dates
premiere_date TEXT,
production_year INTEGER,
date_created TEXT,
-- Runtime (ticks)
runtime_ticks INTEGER,
-- Images
primary_image_tag TEXT,
backdrop_image_tags TEXT, -- JSON array
-- Ratings
community_rating REAL,
official_rating TEXT,
-- Sync metadata
synced_at TEXT,
etag TEXT,
UNIQUE(server_id, id)
);
-- Full-text search index for items
CREATE VIRTUAL TABLE IF NOT EXISTS items_fts USING fts5(
name,
overview,
album_name,
album_artist,
artists,
series_name,
content='items',
content_rowid='rowid'
);
-- Triggers to keep FTS index in sync
CREATE TRIGGER IF NOT EXISTS items_ai AFTER INSERT ON items BEGIN
INSERT INTO items_fts(rowid, name, overview, album_name, album_artist, artists, series_name)
VALUES (new.rowid, new.name, new.overview, new.album_name, new.album_artist, new.artists, new.series_name);
END;
CREATE TRIGGER IF NOT EXISTS items_ad AFTER DELETE ON items BEGIN
INSERT INTO items_fts(items_fts, rowid, name, overview, album_name, album_artist, artists, series_name)
VALUES('delete', old.rowid, old.name, old.overview, old.album_name, old.album_artist, old.artists, old.series_name);
END;
CREATE TRIGGER IF NOT EXISTS items_au AFTER UPDATE ON items BEGIN
INSERT INTO items_fts(items_fts, rowid, name, overview, album_name, album_artist, artists, series_name)
VALUES('delete', old.rowid, old.name, old.overview, old.album_name, old.album_artist, old.artists, old.series_name);
INSERT INTO items_fts(rowid, name, overview, album_name, album_artist, artists, series_name)
VALUES (new.rowid, new.name, new.overview, new.album_name, new.album_artist, new.artists, new.series_name);
END;
-- Media streams (audio/subtitle tracks)
CREATE TABLE IF NOT EXISTS media_streams (
id INTEGER PRIMARY KEY AUTOINCREMENT,
item_id TEXT NOT NULL REFERENCES items(id) ON DELETE CASCADE,
stream_index INTEGER NOT NULL,
stream_type TEXT NOT NULL, -- Audio, Subtitle, Video
codec TEXT,
language TEXT,
display_title TEXT,
is_default INTEGER DEFAULT 0,
is_forced INTEGER DEFAULT 0,
is_external INTEGER DEFAULT 0,
path TEXT, -- For external subtitles
UNIQUE(item_id, stream_index)
);
-- User-specific data (watch progress, favorites)
CREATE TABLE IF NOT EXISTS user_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
item_id TEXT NOT NULL REFERENCES items(id) ON DELETE CASCADE,
-- Playback state
playback_position_ticks INTEGER DEFAULT 0,
play_count INTEGER DEFAULT 0,
is_played INTEGER DEFAULT 0,
is_favorite INTEGER DEFAULT 0,
-- Timestamps
last_played_at TEXT,
-- Sync status
synced_at TEXT,
pending_sync INTEGER DEFAULT 0, -- 1 if local changes need sync
UNIQUE(user_id, item_id)
);
-- Downloaded media files
CREATE TABLE IF NOT EXISTS downloads (
id INTEGER PRIMARY KEY AUTOINCREMENT,
item_id TEXT NOT NULL REFERENCES items(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- File info
file_path TEXT NOT NULL,
file_size INTEGER,
mime_type TEXT,
-- Download state
status TEXT DEFAULT 'pending', -- pending, downloading, completed, failed, paused
progress REAL DEFAULT 0, -- 0.0 to 1.0
-- Transcoding options used
bitrate INTEGER,
container TEXT,
-- Timestamps
queued_at TEXT DEFAULT CURRENT_TIMESTAMP,
started_at TEXT,
completed_at TEXT,
-- Error tracking
error_message TEXT,
retry_count INTEGER DEFAULT 0,
UNIQUE(item_id, user_id)
);
-- Offline mutation queue (changes to sync back to server)
CREATE TABLE IF NOT EXISTS sync_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Operation details
operation TEXT NOT NULL, -- mark_played, mark_favorite, update_progress, etc.
item_id TEXT,
payload TEXT, -- JSON data for the operation
-- Queue state
status TEXT DEFAULT 'pending', -- pending, processing, completed, failed
retry_count INTEGER DEFAULT 0,
-- Timestamps
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
processed_at TEXT,
-- Error tracking
error_message TEXT
);
-- Cached thumbnails
CREATE TABLE IF NOT EXISTS thumbnails (
id INTEGER PRIMARY KEY AUTOINCREMENT,
item_id TEXT NOT NULL REFERENCES items(id) ON DELETE CASCADE,
image_type TEXT NOT NULL, -- Primary, Backdrop, Thumb, Logo, etc.
image_tag TEXT NOT NULL,
file_path TEXT NOT NULL,
width INTEGER,
height INTEGER,
cached_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(item_id, image_type, image_tag)
);
-- User playlists (local + synced)
CREATE TABLE IF NOT EXISTS playlists (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
is_local INTEGER DEFAULT 0, -- 1 for local-only playlists
jellyfin_id TEXT, -- NULL for local-only
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT
);
-- Playlist items
CREATE TABLE IF NOT EXISTS playlist_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
playlist_id TEXT NOT NULL REFERENCES playlists(id) ON DELETE CASCADE,
item_id TEXT NOT NULL REFERENCES items(id) ON DELETE CASCADE,
sort_order INTEGER NOT NULL,
added_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(playlist_id, item_id)
);
-- Indexes for common queries
CREATE INDEX IF NOT EXISTS idx_items_server ON items(server_id);
CREATE INDEX IF NOT EXISTS idx_items_library ON items(library_id);
CREATE INDEX IF NOT EXISTS idx_items_parent ON items(parent_id);
CREATE INDEX IF NOT EXISTS idx_items_type ON items(item_type);
CREATE INDEX IF NOT EXISTS idx_items_album ON items(album_id);
CREATE INDEX IF NOT EXISTS idx_items_series ON items(series_id);
CREATE INDEX IF NOT EXISTS idx_items_season ON items(season_id);
CREATE INDEX IF NOT EXISTS idx_user_data_user ON user_data(user_id);
CREATE INDEX IF NOT EXISTS idx_user_data_item ON user_data(item_id);
CREATE INDEX IF NOT EXISTS idx_downloads_status ON downloads(status);
CREATE INDEX IF NOT EXISTS idx_sync_queue_status ON sync_queue(status);
CREATE INDEX IF NOT EXISTS idx_thumbnails_item ON thumbnails(item_id);
"#;Expand description
Initial schema migration