mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
Merge pull request #673 from 4gray/build/unpack-libsql-native-bindings-win
build(win): unpack libsql native bindings in ASAR
This commit is contained in:
6 files changed
+313
-590
No files matched your search
@@ -136,4 +136,5 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
ipcRenderer.invoke('DB_REMOVE_RECENT_ITEM', contentId, playlistId),
|
||||
dbGetContentByXtreamId: (xtreamId: number, playlistId: string) =>
|
||||
ipcRenderer.invoke('DB_GET_CONTENT_BY_XTREAM_ID', xtreamId, playlistId),
|
||||
dbDeleteAllPlaylists: () => ipcRenderer.invoke('DB_DELETE_ALL_PLAYLISTS'),
|
||||
});
|
||||
@@ -1,29 +1,25 @@
|
||||
/**
|
||||
* Database connection and initialization for Electron
|
||||
* Uses Drizzle ORM with libSQL (@libsql/client)
|
||||
* Default: local file under Electron userData (file: URL)
|
||||
* Optional: remote instance via env (LIBSQL_URL, LIBSQL_AUTH_TOKEN)
|
||||
* Uses Drizzle ORM with better-sqlite3
|
||||
* Stores database file under ~/.iptvnator/databases/
|
||||
*/
|
||||
|
||||
import { createClient } from '@libsql/client';
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
|
||||
import { drizzle } from 'drizzle-orm/libsql';
|
||||
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';
|
||||
|
||||
let db: LibSQLDatabase<typeof schema> | null = null;
|
||||
let client: ReturnType<typeof createClient> | null = null;
|
||||
let initPromise: Promise<LibSQLDatabase<typeof schema>> | null = null;
|
||||
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
|
||||
* Use a path without spaces to avoid libSQL issues
|
||||
*/
|
||||
function getDatabasePath(): string {
|
||||
// Use home directory with a simple path (no spaces)
|
||||
// This avoids "Application Support" path which has a space
|
||||
const dbDir = join(homedir(), '.iptvnator', 'databases');
|
||||
|
||||
// Ensure the directory exists
|
||||
@@ -33,37 +29,26 @@ function getDatabasePath(): string {
|
||||
|
||||
return join(dbDir, 'iptvnator.db');
|
||||
}
|
||||
/**
|
||||
* Build libSQL connection options (local file by default, remote if env set)
|
||||
*/
|
||||
function getLibsqlConfig(): { url: string; authToken?: string } {
|
||||
const remoteUrl = process.env.LIBSQL_URL?.trim();
|
||||
const authToken = process.env.LIBSQL_AUTH_TOKEN?.trim();
|
||||
if (remoteUrl) {
|
||||
// Remote libSQL (e.g., Turso)
|
||||
return { url: remoteUrl, authToken };
|
||||
}
|
||||
// Local file via libSQL - must use file: scheme
|
||||
const filePath = getDatabasePath();
|
||||
// libSQL requires file: prefix (single slash after colon for absolute paths)
|
||||
return { url: `file:${filePath}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the database connection (async)
|
||||
* Initialize the database connection
|
||||
* Creates tables if they don't exist
|
||||
*/
|
||||
export async function initDatabase(): Promise<LibSQLDatabase<typeof schema>> {
|
||||
export async function initDatabase(): Promise<BetterSQLite3Database<typeof schema>> {
|
||||
if (db) return db;
|
||||
if (initPromise) return initPromise;
|
||||
|
||||
initPromise = (async () => {
|
||||
const cfg = getLibsqlConfig();
|
||||
client = createClient(cfg);
|
||||
const database = drizzle(client, { schema });
|
||||
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
|
||||
await createTables();
|
||||
createTables();
|
||||
|
||||
db = database;
|
||||
return database;
|
||||
@@ -75,11 +60,11 @@ export async function initDatabase(): Promise<LibSQLDatabase<typeof schema>> {
|
||||
/**
|
||||
* Create tables if they don't exist
|
||||
*/
|
||||
async function createTables() {
|
||||
if (!client) return;
|
||||
function createTables() {
|
||||
if (!sqlite) return;
|
||||
|
||||
const createTablesSQL = `
|
||||
CREATE TABLE IF NOT EXISTS playlists (
|
||||
const statements = [
|
||||
`CREATE TABLE IF NOT EXISTS playlists (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
serverUrl TEXT,
|
||||
@@ -96,18 +81,16 @@ async function createTables() {
|
||||
macAddress TEXT,
|
||||
url TEXT,
|
||||
last_usage TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS categories (
|
||||
)`,
|
||||
`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 (
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS content (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
category_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
@@ -117,52 +100,39 @@ async function createTables() {
|
||||
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 (
|
||||
)`,
|
||||
`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 (
|
||||
)`,
|
||||
`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);
|
||||
`;
|
||||
|
||||
// libSQL client doesn't guarantee multi-statement execution in one call
|
||||
// Split on ';' and execute sequentially for reliability
|
||||
const statements = createTablesSQL
|
||||
.split(';')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
)`,
|
||||
`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) {
|
||||
// Add back the semicolon when helpful for parser tolerance
|
||||
await client.execute(stmt);
|
||||
sqlite.exec(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +140,7 @@ async function createTables() {
|
||||
* Get the database instance
|
||||
* Initializes the database if not already initialized
|
||||
*/
|
||||
export async function getDatabase(): Promise<LibSQLDatabase<typeof schema>> {
|
||||
export async function getDatabase(): Promise<BetterSQLite3Database<typeof schema>> {
|
||||
if (db) return db;
|
||||
return initDatabase();
|
||||
}
|
||||
@@ -985,3 +985,31 @@ ipcMain.handle(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete all playlists and related data from SQLite
|
||||
*/
|
||||
ipcMain.handle('DB_DELETE_ALL_PLAYLISTS', async () => {
|
||||
try {
|
||||
const db = await getDatabase();
|
||||
|
||||
// Delete in order respecting foreign key constraints
|
||||
// First delete favorites and recently_viewed (they reference content)
|
||||
await db.delete(schema.favorites);
|
||||
await db.delete(schema.recentlyViewed);
|
||||
|
||||
// Then delete content (references categories)
|
||||
await db.delete(schema.content);
|
||||
|
||||
// Then delete categories (references playlists)
|
||||
await db.delete(schema.categories);
|
||||
|
||||
// Finally delete playlists
|
||||
await db.delete(schema.playlists);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error deleting all playlists:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from 'rxjs';
|
||||
import { DataService, EpgService, PlaylistsService } from 'services';
|
||||
import {
|
||||
GLOBAL_FAVORITES_PLAYLIST_ID,
|
||||
OPEN_MPV_PLAYER,
|
||||
OPEN_VLC_PLAYER,
|
||||
Playlist,
|
||||
@@ -71,6 +72,10 @@ export class PlaylistEffects {
|
||||
return this.actions$.pipe(
|
||||
ofType(PlaylistActions.setFavorites),
|
||||
combineLatestWith(this.store.select(selectActivePlaylistId)),
|
||||
filter(
|
||||
([, playlistId]) =>
|
||||
playlistId !== GLOBAL_FAVORITES_PLAYLIST_ID
|
||||
),
|
||||
switchMap(([action, playlistId]) =>
|
||||
this.playlistsService.setFavorites(
|
||||
playlistId,
|
||||
@@ -207,9 +212,25 @@ export class PlaylistEffects {
|
||||
() => {
|
||||
return this.actions$.pipe(
|
||||
ofType(PlaylistActions.removePlaylist),
|
||||
switchMap((action) =>
|
||||
this.playlistsService.deletePlaylist(action.playlistId)
|
||||
)
|
||||
switchMap(async (action) => {
|
||||
// Delete from IndexedDB
|
||||
await firstValueFrom(
|
||||
this.playlistsService.deletePlaylist(action.playlistId)
|
||||
);
|
||||
// Also delete from SQLite if running in Electron
|
||||
if ((window as any).electron?.dbDeletePlaylist) {
|
||||
try {
|
||||
await (window as any).electron.dbDeletePlaylist(
|
||||
action.playlistId
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error deleting playlist from SQLite:',
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
{ dispatch: false }
|
||||
@@ -350,8 +371,20 @@ export class PlaylistEffects {
|
||||
() => {
|
||||
return this.actions$.pipe(
|
||||
ofType(PlaylistActions.removeAllPlaylists),
|
||||
switchMap(() => this.playlistsService.removeAll()),
|
||||
tap(() => {
|
||||
switchMap(async () => {
|
||||
// Delete from IndexedDB
|
||||
await firstValueFrom(this.playlistsService.removeAll());
|
||||
// Also delete from SQLite if running in Electron
|
||||
if ((window as any).electron?.dbDeleteAllPlaylists) {
|
||||
try {
|
||||
await (window as any).electron.dbDeleteAllPlaylists();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error deleting playlists from SQLite:',
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
this.snackBar.open(
|
||||
this.translate.instant('SETTINGS.PLAYLISTS_REMOVED'),
|
||||
undefined,
|
||||
|
||||
Generated
+194
-506
File diff suppressed because it is too large.
Load diff
+6
-3
@@ -33,6 +33,9 @@
|
||||
"remote-control-web/**/*",
|
||||
"!**/*.map"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"**/better-sqlite3/**"
|
||||
],
|
||||
"extraMetadata": {
|
||||
"main": "electron-backend/main.js"
|
||||
},
|
||||
@@ -92,7 +95,6 @@
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": [
|
||||
"ia32",
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
@@ -129,7 +131,7 @@
|
||||
"@angular/platform-browser": "~20.3.10",
|
||||
"@angular/platform-browser-dynamic": "~20.3.10",
|
||||
"@angular/router": "~20.3.10",
|
||||
"@libsql/client": "0.15.15",
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"@ngrx/component-store": "20.0.0",
|
||||
"@ngrx/effects": "20.0.0",
|
||||
"@ngrx/entity": "20.0.0",
|
||||
@@ -147,7 +149,6 @@
|
||||
"date-fns": "4.1.0",
|
||||
"drizzle-orm": "0.44.6",
|
||||
"electron-conf": "1.3.0",
|
||||
"electron-store": "11.0.2",
|
||||
"epg-parser": "0.4.0",
|
||||
"fix-path": "5.0.0",
|
||||
"hls.js": "1.6.13",
|
||||
@@ -206,6 +207,7 @@
|
||||
"@swc-node/register": "~1.9.1",
|
||||
"@swc/core": "~1.5.7",
|
||||
"@swc/helpers": "~0.5.11",
|
||||
"@types/better-sqlite3": "^7.6.12",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/mocha": "9.0.0",
|
||||
@@ -221,6 +223,7 @@
|
||||
"drizzle-kit": "0.31.5",
|
||||
"electron": "^39.0.0",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-playwright-helpers": "1.8.2",
|
||||
"esbuild": "0.27.0",
|
||||
"eslint": "^9.8.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
|
||||
Reference in new issue
Block a user