191 lines
6.4 KiB
JavaScript
191 lines
6.4 KiB
JavaScript
const Database = require('better-sqlite3');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
const dataDir = path.join(__dirname, '..', '..', 'data');
|
|
const dbPath = path.join(dataDir, 'content.db');
|
|
|
|
// Ensure data directory exists
|
|
if (!fs.existsSync(dataDir)) {
|
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
}
|
|
|
|
let db;
|
|
|
|
function getDb() {
|
|
if (!db) {
|
|
console.log('[SQLite] Opening database at', dbPath);
|
|
db = new Database(dbPath);
|
|
// Optimize performance
|
|
db.pragma('journal_mode = WAL');
|
|
db.pragma('synchronous = NORMAL');
|
|
initSchema();
|
|
}
|
|
return db;
|
|
}
|
|
|
|
function initSchema() {
|
|
if (!db) throw new Error('Database not initialized');
|
|
|
|
// Categories (Groups)
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS categories (
|
|
id TEXT PRIMARY KEY, -- Composite key: sourceId:categoryId
|
|
source_id INTEGER NOT NULL,
|
|
category_id TEXT NOT NULL,
|
|
type TEXT NOT NULL, -- 'live', 'movie', 'series'
|
|
name TEXT NOT NULL,
|
|
parent_id TEXT, -- For nested categories
|
|
is_hidden INTEGER DEFAULT 0,
|
|
data JSON -- Extra provider data
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_categories_source_type ON categories(source_id, type);
|
|
`);
|
|
|
|
// Playlist Items (Channels, Movies, Series, Episodes)
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS playlist_items (
|
|
id TEXT PRIMARY KEY, -- Composite key: sourceId:itemId
|
|
source_id INTEGER NOT NULL,
|
|
item_id TEXT NOT NULL, -- Original ID from provider
|
|
type TEXT NOT NULL, -- 'live', 'movie', 'series', 'episode'
|
|
name TEXT NOT NULL,
|
|
category_id TEXT, -- maps to categories.category_id (not our composite id)
|
|
parent_id TEXT, -- For episodes -> series_id
|
|
|
|
-- Common Media Fields
|
|
stream_icon TEXT,
|
|
stream_url TEXT, -- Direct link if available
|
|
container_extension TEXT,
|
|
|
|
-- VOD/Series Specific
|
|
rating REAL,
|
|
year TEXT,
|
|
added_at TEXT,
|
|
|
|
-- App State
|
|
is_hidden INTEGER DEFAULT 0,
|
|
is_favorite INTEGER DEFAULT 0,
|
|
|
|
data JSON -- Full original JSON object
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_items_source_type ON playlist_items(source_id, type);
|
|
CREATE INDEX IF NOT EXISTS idx_items_category ON playlist_items(source_id, category_id);
|
|
`);
|
|
|
|
// EPG Programs
|
|
// Optimized for range queries
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS epg_programs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
channel_id TEXT NOT NULL, -- matches playlist_items.id if possible, or mapping key
|
|
source_id INTEGER NOT NULL,
|
|
start_time INTEGER NOT NULL, -- Unix timestamp (ms)
|
|
end_time INTEGER NOT NULL, -- Unix timestamp (ms)
|
|
title TEXT,
|
|
description TEXT,
|
|
data JSON
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_epg_channel_time ON epg_programs(channel_id, start_time, end_time);
|
|
CREATE INDEX IF NOT EXISTS idx_epg_cleanup ON epg_programs(end_time); -- For deleting old programs
|
|
`);
|
|
|
|
// Sync Status
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS sync_status (
|
|
source_id INTEGER NOT NULL,
|
|
type TEXT NOT NULL, -- 'live', 'vod', 'series', 'epg'
|
|
last_sync INTEGER NOT NULL,
|
|
status TEXT, -- 'success', 'error', 'syncing'
|
|
error TEXT,
|
|
PRIMARY KEY (source_id, type)
|
|
);
|
|
`);
|
|
|
|
// User Favorites (per-user)
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS favorites (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
source_id INTEGER NOT NULL,
|
|
item_id TEXT NOT NULL,
|
|
item_type TEXT NOT NULL, -- 'channel', 'movie', 'series'
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
UNIQUE(user_id, source_id, item_id, item_type)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_favorites_user ON favorites(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_favorites_user_type ON favorites(user_id, item_type);
|
|
`);
|
|
|
|
console.log('[SQLite] Schema initialized');
|
|
}
|
|
|
|
// ============================================================
|
|
// Favorites CRUD Operations
|
|
// ============================================================
|
|
const favorites = {
|
|
getAll(userId, sourceId = null, itemType = null) {
|
|
const db = getDb();
|
|
let sql = 'SELECT * FROM favorites WHERE user_id = ?';
|
|
const params = [userId];
|
|
|
|
if (sourceId) {
|
|
sql += ' AND source_id = ?';
|
|
params.push(sourceId);
|
|
}
|
|
if (itemType) {
|
|
sql += ' AND item_type = ?';
|
|
params.push(itemType);
|
|
}
|
|
|
|
sql += ' ORDER BY created_at DESC';
|
|
return db.prepare(sql).all(...params);
|
|
},
|
|
|
|
add(userId, sourceId, itemId, itemType = 'channel') {
|
|
const db = getDb();
|
|
const stmt = db.prepare(`
|
|
INSERT OR IGNORE INTO favorites (user_id, source_id, item_id, item_type)
|
|
VALUES (?, ?, ?, ?)
|
|
`);
|
|
const result = stmt.run(userId, sourceId, itemId, itemType);
|
|
return result.changes > 0;
|
|
},
|
|
|
|
remove(userId, sourceId, itemId, itemType = 'channel') {
|
|
const db = getDb();
|
|
const stmt = db.prepare(`
|
|
DELETE FROM favorites
|
|
WHERE user_id = ? AND source_id = ? AND item_id = ? AND item_type = ?
|
|
`);
|
|
const result = stmt.run(userId, sourceId, itemId, itemType);
|
|
return result.changes > 0;
|
|
},
|
|
|
|
isFavorite(userId, sourceId, itemId, itemType = 'channel') {
|
|
const db = getDb();
|
|
const row = db.prepare(`
|
|
SELECT 1 FROM favorites
|
|
WHERE user_id = ? AND source_id = ? AND item_id = ? AND item_type = ?
|
|
`).get(userId, sourceId, itemId, itemType);
|
|
return !!row;
|
|
},
|
|
|
|
// Get all favorites for a user, grouped by type (for bulk checks)
|
|
getAllAsSet(userId) {
|
|
const db = getDb();
|
|
const rows = db.prepare('SELECT source_id, item_id, item_type FROM favorites WHERE user_id = ?').all(userId);
|
|
const set = new Set();
|
|
for (const row of rows) {
|
|
set.add(`${row.source_id}:${row.item_id}:${row.item_type}`);
|
|
}
|
|
return set;
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
getDb,
|
|
initSchema,
|
|
favorites
|
|
};
|