mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
Merge pull request #701 from 4gray/refactor/database-library
feat(db): add shared database library with sql config
This commit is contained in:
11 files changed
+474
-253
No files matched your search
+17
-4
@@ -1,9 +1,22 @@
|
||||
# Minimal configuration for getting started
|
||||
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
|
||||
|
||||
language: 'en-US'
|
||||
|
||||
reviews:
|
||||
profile: 'chill'
|
||||
high_level_summary: true
|
||||
auto_review:
|
||||
enabled: true
|
||||
drafts: false
|
||||
auto_review:
|
||||
enabled: true
|
||||
drafts: false
|
||||
|
||||
issue_enrichment:
|
||||
labeling:
|
||||
auto_apply_labels: true
|
||||
labeling_instructions:
|
||||
- label: bug
|
||||
instructions: Issues reporting bugs, errors, crashes, incorrect behavior, or unexpected results. This includes runtime errors, logic errors, broken functionality, regressions, and any deviation from expected or documented behavior.
|
||||
- label: enhancement
|
||||
instructions: Feature requests, improvements to existing functionality, performance optimizations, refactoring suggestions, UI/UX enhancements, and any suggestions to make the project better or add new capabilities.
|
||||
- label: documentation
|
||||
instructions: Documentation updates, additions, corrections, or clarifications needed. This includes missing docs, outdated information, unclear instructions, API documentation, code examples, README improvements, and any requests for better explanations or guides.
|
||||
|
||||
@@ -1,146 +1,14 @@
|
||||
/**
|
||||
* Database connection and initialization for Electron
|
||||
* Uses Drizzle ORM with better-sqlite3
|
||||
* Stores database file under ~/.iptvnator/databases/
|
||||
* Database connection re-export from shared library
|
||||
* This file maintains backwards compatibility for existing imports
|
||||
*/
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
|
||||
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
import * as schema from './schema';
|
||||
export {
|
||||
getDatabase,
|
||||
initDatabase,
|
||||
getDatabasePath,
|
||||
closeDatabase,
|
||||
type DatabaseInstance,
|
||||
type DatabaseOptions,
|
||||
} from 'database';
|
||||
|
||||
let db: BetterSQLite3Database<typeof schema> | null = null;
|
||||
let sqlite: Database.Database | null = null;
|
||||
let initPromise: Promise<BetterSQLite3Database<typeof schema>> | null = null;
|
||||
|
||||
/**
|
||||
* Get the database file path
|
||||
*/
|
||||
function getDatabasePath(): string {
|
||||
const dbDir = join(homedir(), '.iptvnator', 'databases');
|
||||
|
||||
// Ensure the directory exists
|
||||
if (!existsSync(dbDir)) {
|
||||
mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
return join(dbDir, 'iptvnator.db');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the database connection
|
||||
* Creates tables if they don't exist
|
||||
*/
|
||||
export async function initDatabase(): Promise<BetterSQLite3Database<typeof schema>> {
|
||||
if (db) return db;
|
||||
if (initPromise) return initPromise;
|
||||
|
||||
initPromise = (async () => {
|
||||
const filePath = getDatabasePath();
|
||||
sqlite = new Database(filePath);
|
||||
|
||||
// Enable foreign keys
|
||||
sqlite.pragma('foreign_keys = ON');
|
||||
|
||||
const database = drizzle(sqlite, { schema });
|
||||
|
||||
// Create tables if they don't exist
|
||||
createTables();
|
||||
|
||||
db = database;
|
||||
return database;
|
||||
})();
|
||||
|
||||
return initPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create tables if they don't exist
|
||||
*/
|
||||
function createTables() {
|
||||
if (!sqlite) return;
|
||||
|
||||
const statements = [
|
||||
`CREATE TABLE IF NOT EXISTS playlists (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
serverUrl TEXT,
|
||||
username TEXT,
|
||||
password TEXT,
|
||||
date_created TEXT DEFAULT (datetime('now')),
|
||||
last_updated TEXT,
|
||||
type TEXT NOT NULL CHECK (type IN ('xtream', 'stalker', 'm3u-file', 'm3u-text', 'm3u-url')),
|
||||
userAgent TEXT,
|
||||
origin TEXT,
|
||||
referrer TEXT,
|
||||
filePath TEXT,
|
||||
autoRefresh INTEGER DEFAULT 0,
|
||||
macAddress TEXT,
|
||||
url TEXT,
|
||||
last_usage TEXT
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
playlist_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL CHECK (type IN ('live', 'movies', 'series')),
|
||||
xtream_id INTEGER NOT NULL,
|
||||
FOREIGN KEY (playlist_id) REFERENCES playlists (id) ON DELETE CASCADE
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS content (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
category_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
rating TEXT,
|
||||
added TEXT,
|
||||
poster_url TEXT,
|
||||
xtream_id INTEGER NOT NULL,
|
||||
type TEXT NOT NULL CHECK (type IN ('live', 'movie', 'series')),
|
||||
FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE CASCADE
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS recently_viewed (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
content_id INTEGER NOT NULL,
|
||||
playlist_id TEXT NOT NULL,
|
||||
viewed_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (content_id) REFERENCES content (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (playlist_id) REFERENCES playlists (id) ON DELETE CASCADE
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS favorites (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
content_id INTEGER NOT NULL,
|
||||
playlist_id TEXT NOT NULL,
|
||||
added_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (content_id) REFERENCES content(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_content_type ON content(type)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_content_category ON content(category_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_categories_playlist ON categories(playlist_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_content_title ON content(title)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_content_xtream ON content(xtream_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_categories_type ON categories(type)`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS favorites_content_playlist_unique ON favorites(content_id, playlist_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS favorites_playlist_idx ON favorites(playlist_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS favorites_content_idx ON favorites(content_id)`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS recently_viewed_content_playlist_unique ON recently_viewed(content_id, playlist_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS recently_viewed_playlist_idx ON recently_viewed(playlist_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS recently_viewed_viewed_at_idx ON recently_viewed(viewed_at)`,
|
||||
];
|
||||
|
||||
for (const stmt of statements) {
|
||||
sqlite.exec(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the database instance
|
||||
* Initializes the database if not already initialized
|
||||
*/
|
||||
export async function getDatabase(): Promise<BetterSQLite3Database<typeof schema>> {
|
||||
if (db) return db;
|
||||
return initDatabase();
|
||||
}
|
||||
@@ -1,110 +1,25 @@
|
||||
/**
|
||||
* Drizzle ORM schema for IPTVnator database
|
||||
* This schema defines the structure for Xtream Codes API data storage
|
||||
* Database schema re-export from shared library
|
||||
* This file maintains backwards compatibility for existing imports
|
||||
*/
|
||||
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
export {
|
||||
// Tables
|
||||
playlists,
|
||||
categories,
|
||||
content,
|
||||
recentlyViewed,
|
||||
favorites,
|
||||
// Types
|
||||
type Playlist,
|
||||
type NewPlaylist,
|
||||
type Category,
|
||||
type NewCategory,
|
||||
type Content,
|
||||
type NewContent,
|
||||
type RecentlyViewed,
|
||||
type NewRecentlyViewed,
|
||||
type Favorite,
|
||||
type NewFavorite,
|
||||
} from 'database';
|
||||
|
||||
// Playlists table
|
||||
export const playlists = sqliteTable('playlists', {
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
serverUrl: text('serverUrl'),
|
||||
username: text('username'),
|
||||
password: text('password'),
|
||||
dateCreated: text('date_created').default(sql`CURRENT_TIMESTAMP`),
|
||||
lastUpdated: text('last_updated'),
|
||||
type: text('type', {
|
||||
enum: ['xtream', 'stalker', 'm3u-file', 'm3u-text', 'm3u-url'],
|
||||
}).notNull(),
|
||||
userAgent: text('userAgent'),
|
||||
origin: text('origin'),
|
||||
referrer: text('referrer'),
|
||||
filePath: text('filePath'),
|
||||
autoRefresh: integer('autoRefresh', { mode: 'boolean' }).default(false),
|
||||
macAddress: text('macAddress'),
|
||||
url: text('url'),
|
||||
lastUsage: text('last_usage'),
|
||||
});
|
||||
|
||||
// Categories table
|
||||
export const categories = sqliteTable(
|
||||
'categories',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
playlistId: text('playlist_id')
|
||||
.notNull()
|
||||
.references(() => playlists.id, { onDelete: 'cascade' }),
|
||||
name: text('name').notNull(),
|
||||
type: text('type', { enum: ['live', 'movies', 'series'] }).notNull(),
|
||||
xtreamId: integer('xtream_id').notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
playlistIdx: index('idx_categories_playlist').on(table.playlistId),
|
||||
typeIdx: index('idx_categories_type').on(table.type),
|
||||
})
|
||||
);
|
||||
|
||||
// Content table (streams/VODs/series)
|
||||
export const content = sqliteTable(
|
||||
'content',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
categoryId: integer('category_id')
|
||||
.notNull()
|
||||
.references(() => categories.id, { onDelete: 'cascade' }),
|
||||
title: text('title').notNull(),
|
||||
rating: text('rating'),
|
||||
added: text('added'),
|
||||
posterUrl: text('poster_url'),
|
||||
xtreamId: integer('xtream_id').notNull(),
|
||||
type: text('type', { enum: ['live', 'movie', 'series'] }).notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
typeIdx: index('idx_content_type').on(table.type),
|
||||
categoryIdx: index('idx_content_category').on(table.categoryId),
|
||||
titleIdx: index('idx_content_title').on(table.title),
|
||||
xtreamIdx: index('idx_content_xtream').on(table.xtreamId),
|
||||
})
|
||||
);
|
||||
|
||||
// Recently viewed table
|
||||
export const recentlyViewed = sqliteTable('recently_viewed', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
contentId: integer('content_id')
|
||||
.notNull()
|
||||
.references(() => content.id, { onDelete: 'cascade' }),
|
||||
playlistId: text('playlist_id')
|
||||
.notNull()
|
||||
.references(() => playlists.id, { onDelete: 'cascade' }),
|
||||
viewedAt: text('viewed_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
});
|
||||
|
||||
// Favorites table
|
||||
export const favorites = sqliteTable('favorites', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
contentId: integer('content_id')
|
||||
.notNull()
|
||||
.references(() => content.id, { onDelete: 'cascade' }),
|
||||
playlistId: text('playlist_id')
|
||||
.notNull()
|
||||
.references(() => playlists.id, { onDelete: 'cascade' }),
|
||||
addedAt: text('added_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
});
|
||||
|
||||
// Type exports for TypeScript
|
||||
export type Playlist = typeof playlists.$inferSelect;
|
||||
export type NewPlaylist = typeof playlists.$inferInsert;
|
||||
|
||||
export type Category = typeof categories.$inferSelect;
|
||||
export type NewCategory = typeof categories.$inferInsert;
|
||||
|
||||
export type Content = typeof content.$inferSelect;
|
||||
export type NewContent = typeof content.$inferInsert;
|
||||
|
||||
export type RecentlyViewed = typeof recentlyViewed.$inferSelect;
|
||||
export type NewRecentlyViewed = typeof recentlyViewed.$inferInsert;
|
||||
|
||||
export type Favorite = typeof favorites.$inferSelect;
|
||||
export type NewFavorite = typeof favorites.$inferInsert;
|
||||
@@ -0,0 +1,33 @@
|
||||
# Database
|
||||
|
||||
Shared database library for IPTVnator providing Drizzle ORM schema and connection utilities.
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
// Full access (electron-backend)
|
||||
import { getDatabase, initDatabase } from 'database';
|
||||
|
||||
// Read-only access (agent-backend)
|
||||
import { getReadOnlyDatabase } from 'database';
|
||||
|
||||
// Schema and types
|
||||
import { content, categories, playlists, type Content } from 'database';
|
||||
```
|
||||
|
||||
## Exports
|
||||
|
||||
### Schema (`schema.ts`)
|
||||
- **Tables**: `playlists`, `categories`, `content`, `recentlyViewed`, `favorites`
|
||||
- **Types**: `Playlist`, `Category`, `Content`, `RecentlyViewed`, `Favorite` (and `New*` variants)
|
||||
|
||||
### Connection (`connection.ts`)
|
||||
- `getDatabase(options?)` - Full read-write access
|
||||
- `getReadOnlyDatabase()` - Read-only access for agent queries
|
||||
- `initDatabase(options?)` - Initialize with custom options
|
||||
- `closeDatabase()` - Close connection
|
||||
- `getDatabasePath()` - Get database file path
|
||||
|
||||
## Database Location
|
||||
|
||||
The SQLite database is stored at: `~/.iptvnator/databases/iptvnator.db`
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "database",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/shared/database/src",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:shared", "type:data-access"],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/esbuild:esbuild",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/libs/shared/database",
|
||||
"main": "libs/shared/database/src/index.ts",
|
||||
"tsConfig": "libs/shared/database/tsconfig.lib.json",
|
||||
"assets": ["libs/shared/database/*.md"],
|
||||
"format": ["cjs"]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"command": "eslint libs/shared/database/**/*.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Shared Database module for IPTVnator
|
||||
* Provides Drizzle ORM schema and connection utilities for SQLite database
|
||||
* Used by both electron-backend (read-write) and agent-backend (read-only)
|
||||
*/
|
||||
|
||||
export * from './lib/schema';
|
||||
export * from './lib/connection';
|
||||
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Database connection and initialization for IPTVnator
|
||||
* Uses Drizzle ORM with better-sqlite3
|
||||
* Stores database file under ~/.iptvnator/databases/
|
||||
*
|
||||
* Provides two connection modes:
|
||||
* - Full access (for electron-backend): creates tables, read-write
|
||||
* - Read-only access (for agent-backend): no table creation, read-only queries
|
||||
*/
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
|
||||
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
import * as schema from './schema';
|
||||
|
||||
export type DatabaseInstance = BetterSQLite3Database<typeof schema>;
|
||||
|
||||
let db: DatabaseInstance | null = null;
|
||||
let sqlite: Database.Database | null = null;
|
||||
let initPromise: Promise<DatabaseInstance> | null = null;
|
||||
|
||||
/**
|
||||
* Get the database file path
|
||||
*/
|
||||
export function getDatabasePath(): string {
|
||||
const dbDir = join(homedir(), '.iptvnator', 'databases');
|
||||
|
||||
// Ensure the directory exists
|
||||
if (!existsSync(dbDir)) {
|
||||
mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
return join(dbDir, 'iptvnator.db');
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL statements for creating all tables
|
||||
*/
|
||||
const CREATE_TABLE_STATEMENTS = [
|
||||
`CREATE TABLE IF NOT EXISTS playlists (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
serverUrl TEXT,
|
||||
username TEXT,
|
||||
password TEXT,
|
||||
date_created TEXT DEFAULT (datetime('now')),
|
||||
last_updated TEXT,
|
||||
type TEXT NOT NULL CHECK (type IN ('xtream', 'stalker', 'm3u-file', 'm3u-text', 'm3u-url')),
|
||||
userAgent TEXT,
|
||||
origin TEXT,
|
||||
referrer TEXT,
|
||||
filePath TEXT,
|
||||
autoRefresh INTEGER DEFAULT 0,
|
||||
macAddress TEXT,
|
||||
url TEXT,
|
||||
last_usage TEXT
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
playlist_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL CHECK (type IN ('live', 'movies', 'series')),
|
||||
xtream_id INTEGER NOT NULL,
|
||||
FOREIGN KEY (playlist_id) REFERENCES playlists (id) ON DELETE CASCADE
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS content (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
category_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
rating TEXT,
|
||||
added TEXT,
|
||||
poster_url TEXT,
|
||||
xtream_id INTEGER NOT NULL,
|
||||
type TEXT NOT NULL CHECK (type IN ('live', 'movie', 'series')),
|
||||
FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE CASCADE
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS recently_viewed (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
content_id INTEGER NOT NULL,
|
||||
playlist_id TEXT NOT NULL,
|
||||
viewed_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (content_id) REFERENCES content (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (playlist_id) REFERENCES playlists (id) ON DELETE CASCADE
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS favorites (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
content_id INTEGER NOT NULL,
|
||||
playlist_id TEXT NOT NULL,
|
||||
added_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (content_id) REFERENCES content(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_content_type ON content(type)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_content_category ON content(category_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_categories_playlist ON categories(playlist_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_content_title ON content(title)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_content_xtream ON content(xtream_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_categories_type ON categories(type)`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS favorites_content_playlist_unique ON favorites(content_id, playlist_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS favorites_playlist_idx ON favorites(playlist_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS favorites_content_idx ON favorites(content_id)`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS recently_viewed_content_playlist_unique ON recently_viewed(content_id, playlist_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS recently_viewed_playlist_idx ON recently_viewed(playlist_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS recently_viewed_viewed_at_idx ON recently_viewed(viewed_at)`,
|
||||
];
|
||||
|
||||
/**
|
||||
* Create tables if they don't exist
|
||||
*/
|
||||
function createTables(sqliteDb: Database.Database): void {
|
||||
for (const stmt of CREATE_TABLE_STATEMENTS) {
|
||||
sqliteDb.exec(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
export interface DatabaseOptions {
|
||||
/** Open database in read-only mode (for agent-backend) */
|
||||
readonly?: boolean;
|
||||
/** Skip table creation (useful for read-only connections) */
|
||||
skipTableCreation?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the database connection with full access (read-write)
|
||||
* Creates tables if they don't exist
|
||||
* Used by electron-backend
|
||||
*/
|
||||
export async function initDatabase(
|
||||
options: DatabaseOptions = {}
|
||||
): Promise<DatabaseInstance> {
|
||||
const { readonly = false, skipTableCreation = false } = options;
|
||||
|
||||
if (db) return db;
|
||||
if (initPromise) return initPromise;
|
||||
|
||||
initPromise = (async () => {
|
||||
const filePath = getDatabasePath();
|
||||
sqlite = new Database(filePath, { readonly });
|
||||
|
||||
// Enable foreign keys
|
||||
sqlite.pragma('foreign_keys = ON');
|
||||
|
||||
// Create tables only for read-write connections
|
||||
if (!readonly && !skipTableCreation) {
|
||||
createTables(sqlite);
|
||||
}
|
||||
|
||||
const database = drizzle(sqlite, { schema });
|
||||
db = database;
|
||||
return database;
|
||||
})();
|
||||
|
||||
return initPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the database instance
|
||||
* Initializes the database if not already initialized (with defaults for full access)
|
||||
* Used by electron-backend
|
||||
*/
|
||||
export async function getDatabase(
|
||||
options?: DatabaseOptions
|
||||
): Promise<DatabaseInstance> {
|
||||
if (db) return db;
|
||||
return initDatabase(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a read-only database connection
|
||||
* Used by agent-backend for safe read-only queries
|
||||
*/
|
||||
export async function getReadOnlyDatabase(): Promise<DatabaseInstance> {
|
||||
return getDatabase({ readonly: true, skipTableCreation: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection
|
||||
*/
|
||||
export function closeDatabase(): void {
|
||||
if (sqlite) {
|
||||
sqlite.close();
|
||||
sqlite = null;
|
||||
db = null;
|
||||
initPromise = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Drizzle ORM schema for IPTVnator database
|
||||
* This schema defines the structure for Xtream Codes API data storage
|
||||
*
|
||||
* Single source of truth for database schema - used by:
|
||||
* - electron-backend (full read-write access)
|
||||
* - agent-backend (read-only access for AI queries)
|
||||
*/
|
||||
|
||||
import { sql } from 'drizzle-orm';
|
||||
import {
|
||||
index,
|
||||
integer,
|
||||
sqliteTable,
|
||||
text,
|
||||
uniqueIndex,
|
||||
} from 'drizzle-orm/sqlite-core';
|
||||
|
||||
// Playlists table
|
||||
export const playlists = sqliteTable('playlists', {
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
serverUrl: text('serverUrl'),
|
||||
username: text('username'),
|
||||
password: text('password'),
|
||||
dateCreated: text('date_created').default(sql`CURRENT_TIMESTAMP`),
|
||||
lastUpdated: text('last_updated'),
|
||||
type: text('type', {
|
||||
enum: ['xtream', 'stalker', 'm3u-file', 'm3u-text', 'm3u-url'],
|
||||
}).notNull(),
|
||||
userAgent: text('userAgent'),
|
||||
origin: text('origin'),
|
||||
referrer: text('referrer'),
|
||||
filePath: text('filePath'),
|
||||
autoRefresh: integer('autoRefresh', { mode: 'boolean' }).default(false),
|
||||
macAddress: text('macAddress'),
|
||||
url: text('url'),
|
||||
lastUsage: text('last_usage'),
|
||||
});
|
||||
|
||||
// Categories table
|
||||
export const categories = sqliteTable(
|
||||
'categories',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
playlistId: text('playlist_id')
|
||||
.notNull()
|
||||
.references(() => playlists.id, { onDelete: 'cascade' }),
|
||||
name: text('name').notNull(),
|
||||
type: text('type', { enum: ['live', 'movies', 'series'] }).notNull(),
|
||||
xtreamId: integer('xtream_id').notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
playlistIdx: index('idx_categories_playlist').on(table.playlistId),
|
||||
typeIdx: index('idx_categories_type').on(table.type),
|
||||
})
|
||||
);
|
||||
|
||||
// Content table (streams/VODs/series)
|
||||
export const content = sqliteTable(
|
||||
'content',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
categoryId: integer('category_id')
|
||||
.notNull()
|
||||
.references(() => categories.id, { onDelete: 'cascade' }),
|
||||
title: text('title').notNull(),
|
||||
rating: text('rating'),
|
||||
added: text('added'),
|
||||
posterUrl: text('poster_url'),
|
||||
xtreamId: integer('xtream_id').notNull(),
|
||||
type: text('type', { enum: ['live', 'movie', 'series'] }).notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
typeIdx: index('idx_content_type').on(table.type),
|
||||
categoryIdx: index('idx_content_category').on(table.categoryId),
|
||||
titleIdx: index('idx_content_title').on(table.title),
|
||||
xtreamIdx: index('idx_content_xtream').on(table.xtreamId),
|
||||
})
|
||||
);
|
||||
|
||||
// Recently viewed table
|
||||
export const recentlyViewed = sqliteTable(
|
||||
'recently_viewed',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
contentId: integer('content_id')
|
||||
.notNull()
|
||||
.references(() => content.id, { onDelete: 'cascade' }),
|
||||
playlistId: text('playlist_id')
|
||||
.notNull()
|
||||
.references(() => playlists.id, { onDelete: 'cascade' }),
|
||||
viewedAt: text('viewed_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
},
|
||||
(table) => ({
|
||||
contentPlaylistUnique: uniqueIndex(
|
||||
'recently_viewed_content_playlist_unique'
|
||||
).on(table.contentId, table.playlistId),
|
||||
playlistIdx: index('recently_viewed_playlist_idx').on(table.playlistId),
|
||||
viewedAtIdx: index('recently_viewed_viewed_at_idx').on(table.viewedAt),
|
||||
})
|
||||
);
|
||||
|
||||
// Favorites table
|
||||
export const favorites = sqliteTable(
|
||||
'favorites',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
contentId: integer('content_id')
|
||||
.notNull()
|
||||
.references(() => content.id, { onDelete: 'cascade' }),
|
||||
playlistId: text('playlist_id')
|
||||
.notNull()
|
||||
.references(() => playlists.id, { onDelete: 'cascade' }),
|
||||
addedAt: text('added_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
},
|
||||
(table) => ({
|
||||
contentPlaylistUnique: uniqueIndex(
|
||||
'favorites_content_playlist_unique'
|
||||
).on(table.contentId, table.playlistId),
|
||||
playlistIdx: index('favorites_playlist_idx').on(table.playlistId),
|
||||
contentIdx: index('favorites_content_idx').on(table.contentId),
|
||||
})
|
||||
);
|
||||
|
||||
// Type exports for TypeScript
|
||||
export type Playlist = typeof playlists.$inferSelect;
|
||||
export type NewPlaylist = typeof playlists.$inferInsert;
|
||||
|
||||
export type Category = typeof categories.$inferSelect;
|
||||
export type NewCategory = typeof categories.$inferInsert;
|
||||
|
||||
export type Content = typeof content.$inferSelect;
|
||||
export type NewContent = typeof content.$inferInsert;
|
||||
|
||||
export type RecentlyViewed = typeof recentlyViewed.$inferSelect;
|
||||
export type NewRecentlyViewed = typeof recentlyViewed.$inferInsert;
|
||||
|
||||
export type Favorite = typeof favorites.$inferSelect;
|
||||
export type NewFavorite = typeof favorites.$inferInsert;
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"importHelpers": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
|
||||
}
|
||||
+2
-1
@@ -23,7 +23,8 @@
|
||||
"services": ["libs/services/src/index.ts"],
|
||||
"shared-interfaces": ["libs/shared/interfaces/src/index.ts"],
|
||||
"shared-portals": ["libs/ui/shared-portals/src/index.ts"],
|
||||
"remote-control": ["libs/ui/remote-control/src/index.ts"]
|
||||
"remote-control": ["libs/ui/remote-control/src/index.ts"],
|
||||
"database": ["libs/shared/database/src/index.ts"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "tmp"],
|
||||
|
||||
Reference in new issue
Block a user