import Database, { Database as DatabaseType } from 'better-sqlite3'; import path from 'path'; import fs from 'fs'; // TypeScript interfaces for cluster feature export interface Cluster { id: number; name: string; description?: string; color: string; icon: string; created_at: string; updated_at: string; } export interface ClusterWithStats extends Cluster { library_count: number; media_count: number; video_count: number; photo_count: number; text_count: number; total_size: number; } export interface LibraryClusterMapping { id: number; library_id: number; cluster_id: number; created_at: string; } let db: DatabaseType | null = null; function initializeDatabase() { if (db) return db; // const dbPath = process.env.DB_FILE || path.join(process.cwd(), 'media.db'); const dbPath = path.join(process.cwd(), 'data', 'media.db'); // Ensure the data directory exists const dataDir = path.dirname(dbPath); if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }); } db = new Database(dbPath); // Create tables db.exec(` CREATE TABLE IF NOT EXISTS libraries ( id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT NOT NULL UNIQUE ); `); db.exec(` CREATE TABLE IF NOT EXISTS media ( id INTEGER PRIMARY KEY AUTOINCREMENT, library_id INTEGER, path TEXT NOT NULL UNIQUE, type TEXT NOT NULL, title TEXT, size INTEGER, thumbnail TEXT, codec_info TEXT DEFAULT '{}', bookmark_count INTEGER DEFAULT 0, star_count INTEGER DEFAULT 0, avg_rating REAL DEFAULT 0.0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (library_id) REFERENCES libraries (id) ); `); db.exec(` CREATE TABLE IF NOT EXISTS bookmarks ( id INTEGER PRIMARY KEY AUTOINCREMENT, media_id INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (media_id) REFERENCES media(id) ON DELETE CASCADE ); `); db.exec(` CREATE TABLE IF NOT EXISTS stars ( id INTEGER PRIMARY KEY AUTOINCREMENT, media_id INTEGER NOT NULL, rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (media_id) REFERENCES media(id) ON DELETE CASCADE ); `); // Create folder bookmarks table db.exec(` CREATE TABLE IF NOT EXISTS folder_bookmarks ( id INTEGER PRIMARY KEY AUTOINCREMENT, folder_path TEXT NOT NULL UNIQUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); `); // Create clusters table db.exec(` CREATE TABLE IF NOT EXISTS clusters ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, description TEXT, color TEXT DEFAULT '#6366f1', icon TEXT DEFAULT 'folder', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); `); // Create library-cluster mapping table db.exec(` CREATE TABLE IF NOT EXISTS library_cluster_mapping ( id INTEGER PRIMARY KEY AUTOINCREMENT, library_id INTEGER NOT NULL, cluster_id INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (library_id) REFERENCES libraries(id) ON DELETE CASCADE, FOREIGN KEY (cluster_id) REFERENCES clusters(id) ON DELETE CASCADE, UNIQUE(library_id, cluster_id) ); `); // Create indexes for performance db.exec(`CREATE INDEX IF NOT EXISTS idx_bookmarks_media_id ON bookmarks(media_id);`); db.exec(`CREATE INDEX IF NOT EXISTS idx_stars_media_id ON stars(media_id);`); db.exec(`CREATE INDEX IF NOT EXISTS idx_folder_bookmarks_path ON folder_bookmarks(folder_path);`); db.exec(`CREATE INDEX IF NOT EXISTS idx_media_bookmark_count ON media(bookmark_count);`); db.exec(`CREATE INDEX IF NOT EXISTS idx_media_star_count ON media(star_count);`); // Pagination and filtering indexes db.exec(`CREATE INDEX IF NOT EXISTS idx_media_type_created_at ON media(type, created_at);`); db.exec(`CREATE INDEX IF NOT EXISTS idx_media_path ON media(path);`); db.exec(`CREATE INDEX IF NOT EXISTS idx_media_library_id ON media(library_id);`); // Full-text search indexes db.exec(`CREATE INDEX IF NOT EXISTS idx_media_title ON media(title);`); db.exec(`CREATE INDEX IF NOT EXISTS idx_media_type_path ON media(type, path);`); // Cluster indexes db.exec(`CREATE INDEX IF NOT EXISTS idx_library_cluster_mapping_library ON library_cluster_mapping(library_id);`); db.exec(`CREATE INDEX IF NOT EXISTS idx_library_cluster_mapping_cluster ON library_cluster_mapping(cluster_id);`); db.exec(`CREATE INDEX IF NOT EXISTS idx_clusters_name ON clusters(name);`); return db; } export function getDatabase(): DatabaseType { return initializeDatabase(); } // Helper functions for folder bookmarks export function addFolderBookmark(folderPath: string): number { const db = getDatabase(); const result = db.prepare(` INSERT OR REPLACE INTO folder_bookmarks (folder_path, updated_at) VALUES (?, CURRENT_TIMESTAMP) `).run(folderPath); return result.lastInsertRowid as number; } export function removeFolderBookmark(folderPath: string): boolean { const db = getDatabase(); const result = db.prepare(` DELETE FROM folder_bookmarks WHERE folder_path = ? `).run(folderPath); return result.changes > 0; } export function isFolderBookmarked(folderPath: string): boolean { const db = getDatabase(); const result = db.prepare(` SELECT id FROM folder_bookmarks WHERE folder_path = ? `).get(folderPath) as { id: number } | undefined; return !!result; } export function getFolderBookmarks(limit: number = 50, offset: number = 0) { const db = getDatabase(); const bookmarks = db.prepare(` SELECT * FROM folder_bookmarks ORDER BY updated_at DESC LIMIT ? OFFSET ? `).all(limit, offset); const totalResult = db.prepare(` SELECT COUNT(*) as total FROM folder_bookmarks `).get() as { total: number }; return { bookmarks, total: totalResult.total }; } // For backward compatibility, export the database instance getter export default getDatabase;