310 lines
9.5 KiB
TypeScript
310 lines
9.5 KiB
TypeScript
import Database from 'better-sqlite3';
|
|
import path from 'path';
|
|
import { Channel, Playlist, Video, Config } from '../types';
|
|
|
|
const dbPath = path.join(__dirname, '../../data.db');
|
|
const db = new Database(dbPath);
|
|
|
|
// Enable foreign keys
|
|
db.pragma('foreign_keys = ON');
|
|
|
|
export function initDatabase() {
|
|
// Create base tables first (without foreign keys that reference tables not yet created)
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS channels (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
url TEXT NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
channelId TEXT NOT NULL,
|
|
addedAt TEXT NOT NULL DEFAULT (datetime('now')),
|
|
lastChecked TEXT,
|
|
active INTEGER NOT NULL DEFAULT 1
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS playlists (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
url TEXT NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
playlistId TEXT NOT NULL,
|
|
addedAt TEXT NOT NULL DEFAULT (datetime('now')),
|
|
lastChecked TEXT,
|
|
active INTEGER NOT NULL DEFAULT 1
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS config (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
key TEXT NOT NULL UNIQUE,
|
|
value TEXT NOT NULL
|
|
);
|
|
`);
|
|
|
|
// Check if videos table exists and if it has playlistId column
|
|
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='videos'").all() as Array<{ name: string }>;
|
|
const videosTableExists = tables.length > 0;
|
|
|
|
if (videosTableExists) {
|
|
// Videos table exists - check if it needs migration
|
|
const tableInfo = db.prepare("PRAGMA table_info(videos)").all() as Array<{ name: string }>;
|
|
const hasPlaylistId = tableInfo.some(col => col.name === 'playlistId');
|
|
|
|
if (!hasPlaylistId) {
|
|
console.log('Migrating database: Adding playlistId column to videos table...');
|
|
db.exec('ALTER TABLE videos ADD COLUMN playlistId INTEGER');
|
|
console.log('Migration completed successfully');
|
|
}
|
|
} else {
|
|
// Videos table doesn't exist - create it with playlistId
|
|
db.exec(`
|
|
CREATE TABLE videos (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
channelId INTEGER,
|
|
playlistId INTEGER,
|
|
videoId TEXT NOT NULL UNIQUE,
|
|
title TEXT NOT NULL,
|
|
url TEXT NOT NULL,
|
|
duration INTEGER,
|
|
thumbnail TEXT,
|
|
uploadDate TEXT,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
filePath TEXT,
|
|
fileSize INTEGER,
|
|
addedAt TEXT NOT NULL DEFAULT (datetime('now')),
|
|
downloadedAt TEXT,
|
|
error TEXT,
|
|
FOREIGN KEY (channelId) REFERENCES channels(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (playlistId) REFERENCES playlists(id) ON DELETE CASCADE
|
|
);
|
|
`);
|
|
}
|
|
|
|
// Create indexes
|
|
db.exec(`
|
|
CREATE INDEX IF NOT EXISTS idx_videos_status ON videos(status);
|
|
CREATE INDEX IF NOT EXISTS idx_videos_channelId ON videos(channelId);
|
|
CREATE INDEX IF NOT EXISTS idx_videos_playlistId ON videos(playlistId);
|
|
`);
|
|
|
|
// Initialize default config
|
|
const defaultConfig = {
|
|
intervalMinutes: '180', // 180 minutes = 3 hours
|
|
varianceMinutes: '30',
|
|
maxConcurrentDownloads: '1',
|
|
enabled: 'true',
|
|
webdavEnabled: 'false',
|
|
webdavUrl: '',
|
|
webdavUsername: '',
|
|
webdavPassword: '',
|
|
webdavPath: '/vidrip/'
|
|
};
|
|
|
|
const insertConfig = db.prepare(
|
|
'INSERT OR IGNORE INTO config (key, value) VALUES (?, ?)'
|
|
);
|
|
|
|
Object.entries(defaultConfig).forEach(([key, value]) => {
|
|
insertConfig.run(key, value);
|
|
});
|
|
|
|
// Migration: Convert intervalHours to intervalMinutes if it exists
|
|
try {
|
|
const intervalHours = configOperations.get('intervalHours');
|
|
if (intervalHours) {
|
|
const hours = parseFloat(intervalHours);
|
|
const minutes = hours * 60;
|
|
console.log(`Migrating config: Converting ${hours} hours to ${minutes} minutes`);
|
|
configOperations.set('intervalMinutes', minutes.toString());
|
|
// Remove old key
|
|
db.prepare('DELETE FROM config WHERE key = ?').run('intervalHours');
|
|
}
|
|
} catch (error) {
|
|
// Ignore migration errors
|
|
}
|
|
|
|
console.log('Database initialized');
|
|
}
|
|
|
|
// Channel operations
|
|
export const channelOperations = {
|
|
getAll: () => {
|
|
return db.prepare('SELECT * FROM channels ORDER BY addedAt DESC').all() as Channel[];
|
|
},
|
|
|
|
getById: (id: number) => {
|
|
return db.prepare('SELECT * FROM channels WHERE id = ?').get(id) as Channel | undefined;
|
|
},
|
|
|
|
create: (url: string, name: string, channelId: string) => {
|
|
const stmt = db.prepare(
|
|
'INSERT INTO channels (url, name, channelId) VALUES (?, ?, ?)'
|
|
);
|
|
const result = stmt.run(url, name, channelId);
|
|
return result.lastInsertRowid as number;
|
|
},
|
|
|
|
update: (id: number, data: Partial<Channel>) => {
|
|
const fields = Object.keys(data).filter(k => k !== 'id');
|
|
const values = fields.map(k => data[k as keyof Channel]);
|
|
const setClause = fields.map(f => `${f} = ?`).join(', ');
|
|
|
|
const stmt = db.prepare(`UPDATE channels SET ${setClause} WHERE id = ?`);
|
|
return stmt.run(...values, id);
|
|
},
|
|
|
|
delete: (id: number) => {
|
|
return db.prepare('DELETE FROM channels WHERE id = ?').run(id);
|
|
},
|
|
|
|
updateLastChecked: (id: number) => {
|
|
return db.prepare(
|
|
"UPDATE channels SET lastChecked = datetime('now') WHERE id = ?"
|
|
).run(id);
|
|
}
|
|
};
|
|
|
|
// Playlist operations
|
|
export const playlistOperations = {
|
|
getAll: () => {
|
|
return db.prepare('SELECT * FROM playlists ORDER BY addedAt DESC').all() as Playlist[];
|
|
},
|
|
|
|
getById: (id: number) => {
|
|
return db.prepare('SELECT * FROM playlists WHERE id = ?').get(id) as Playlist | undefined;
|
|
},
|
|
|
|
create: (url: string, name: string, playlistId: string) => {
|
|
const stmt = db.prepare(
|
|
'INSERT INTO playlists (url, name, playlistId) VALUES (?, ?, ?)'
|
|
);
|
|
const result = stmt.run(url, name, playlistId);
|
|
return result.lastInsertRowid as number;
|
|
},
|
|
|
|
update: (id: number, data: Partial<Playlist>) => {
|
|
const fields = Object.keys(data).filter(k => k !== 'id');
|
|
const values = fields.map(k => data[k as keyof Playlist]);
|
|
const setClause = fields.map(f => `${f} = ?`).join(', ');
|
|
|
|
const stmt = db.prepare(`UPDATE playlists SET ${setClause} WHERE id = ?`);
|
|
return stmt.run(...values, id);
|
|
},
|
|
|
|
delete: (id: number) => {
|
|
return db.prepare('DELETE FROM playlists WHERE id = ?').run(id);
|
|
},
|
|
|
|
updateLastChecked: (id: number) => {
|
|
return db.prepare(
|
|
"UPDATE playlists SET lastChecked = datetime('now') WHERE id = ?"
|
|
).run(id);
|
|
}
|
|
};
|
|
|
|
// Video operations
|
|
export const videoOperations = {
|
|
getAll: () => {
|
|
return db.prepare('SELECT * FROM videos ORDER BY addedAt DESC').all() as Video[];
|
|
},
|
|
|
|
getById: (id: number) => {
|
|
return db.prepare('SELECT * FROM videos WHERE id = ?').get(id) as Video | undefined;
|
|
},
|
|
|
|
getByChannelId: (channelId: number) => {
|
|
return db.prepare('SELECT * FROM videos WHERE channelId = ? ORDER BY addedAt DESC')
|
|
.all(channelId) as Video[];
|
|
},
|
|
|
|
getByPlaylistId: (playlistId: number) => {
|
|
return db.prepare('SELECT * FROM videos WHERE playlistId = ? ORDER BY addedAt DESC')
|
|
.all(playlistId) as Video[];
|
|
},
|
|
|
|
getByStatus: (status: string) => {
|
|
return db.prepare('SELECT * FROM videos WHERE status = ? ORDER BY addedAt ASC')
|
|
.all(status) as Video[];
|
|
},
|
|
|
|
search: (query: string, status?: string) => {
|
|
const searchPattern = `%${query}%`;
|
|
|
|
if (status) {
|
|
return db.prepare(
|
|
'SELECT * FROM videos WHERE title LIKE ? COLLATE NOCASE AND status = ? ORDER BY addedAt DESC'
|
|
).all(searchPattern, status) as Video[];
|
|
} else {
|
|
return db.prepare(
|
|
'SELECT * FROM videos WHERE title LIKE ? COLLATE NOCASE ORDER BY addedAt DESC'
|
|
).all(searchPattern) as Video[];
|
|
}
|
|
},
|
|
|
|
getNextPending: () => {
|
|
return db.prepare('SELECT * FROM videos WHERE status = ? ORDER BY addedAt ASC LIMIT 1')
|
|
.get('pending') as Video | undefined;
|
|
},
|
|
|
|
create: (video: Omit<Video, 'id' | 'addedAt' | 'downloadedAt'>) => {
|
|
const stmt = db.prepare(`
|
|
INSERT INTO videos (channelId, playlistId, videoId, title, url, duration, thumbnail, uploadDate, status, filePath, fileSize, error)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`);
|
|
const result = stmt.run(
|
|
video.channelId,
|
|
video.playlistId,
|
|
video.videoId,
|
|
video.title,
|
|
video.url,
|
|
video.duration,
|
|
video.thumbnail,
|
|
video.uploadDate,
|
|
video.status,
|
|
video.filePath,
|
|
video.fileSize,
|
|
video.error
|
|
);
|
|
return result.lastInsertRowid as number;
|
|
},
|
|
|
|
updateStatus: (id: number, status: string, error?: string) => {
|
|
const errorValue = error !== undefined ? error : null;
|
|
return db.prepare('UPDATE videos SET status = ?, error = ? WHERE id = ?')
|
|
.run(status, errorValue, id);
|
|
},
|
|
|
|
markCompleted: (id: number, filePath: string, fileSize: number) => {
|
|
return db.prepare(`
|
|
UPDATE videos
|
|
SET status = 'completed', filePath = ?, fileSize = ?, downloadedAt = datetime('now')
|
|
WHERE id = ?
|
|
`).run(filePath, fileSize, id);
|
|
},
|
|
|
|
delete: (id: number) => {
|
|
return db.prepare('DELETE FROM videos WHERE id = ?').run(id);
|
|
}
|
|
};
|
|
|
|
// Config operations
|
|
export const configOperations = {
|
|
get: (key: string) => {
|
|
const result = db.prepare('SELECT value FROM config WHERE key = ?').get(key) as Config | undefined;
|
|
return result?.value;
|
|
},
|
|
|
|
set: (key: string, value: string) => {
|
|
return db.prepare('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)')
|
|
.run(key, value);
|
|
},
|
|
|
|
getAll: () => {
|
|
const configs = db.prepare('SELECT * FROM config').all() as Config[];
|
|
const result: Record<string, string> = {};
|
|
configs.forEach(c => {
|
|
result[c.key] = c.value;
|
|
});
|
|
return result;
|
|
}
|
|
};
|
|
|
|
export default db;
|