nextav/src/db/index.ts

206 lines
6.2 KiB
TypeScript

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;