mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
feat(electron): bundle worker assets and resolve worker path
Add webpack config and include it in the electron backend build so worker scripts are correctly bundled for packaged apps. Move worker files to be packaged as extraResources instead of asar unpack patterns so the packaged app can load worker JS from Resources/dist/... at runtime. Fix worker path resolution in epg.events: compute resourcesPath from app.getAppPath() and point to dist/apps/electron-backend/workers when app.isPackaged. Remove verbose app path logging and update comments to reflect the actual packaged layout. Why: ensure webworker entry points are included in the renderer/backend bundle and accessible when the Electron app is packaged, preventing worker loading failures in production builds.
This commit is contained in:
1 parent
b7109c16a2
commit
a948ce0265
7 files changed
+1636
-219
No files matched your search
@@ -0,0 +1,44 @@
|
||||
const esbuild = require('esbuild');
|
||||
const path = require('path');
|
||||
|
||||
// Node.js built-in modules that should be externalized
|
||||
const nodeBuiltins = [
|
||||
'assert', 'async_hooks', 'buffer', 'child_process', 'cluster',
|
||||
'console', 'constants', 'crypto', 'dgram', 'dns', 'domain',
|
||||
'events', 'fs', 'http', 'http2', 'https', 'inspector', 'module',
|
||||
'net', 'os', 'path', 'perf_hooks', 'process', 'punycode',
|
||||
'querystring', 'readline', 'repl', 'stream', 'string_decoder',
|
||||
'sys', 'timers', 'tls', 'trace_events', 'tty', 'url', 'util',
|
||||
'v8', 'vm', 'worker_threads', 'zlib'
|
||||
];
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
async function buildWorker() {
|
||||
try {
|
||||
console.log(`Building EPG parser worker with esbuild (${isProduction ? 'production' : 'development'})...`);
|
||||
|
||||
await esbuild.build({
|
||||
entryPoints: [path.join(__dirname, 'src/app/workers/epg-parser.worker.ts')],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
target: 'node18',
|
||||
format: 'cjs',
|
||||
outfile: path.join(__dirname, '../../dist/apps/electron-backend/workers/epg-parser.worker.js'),
|
||||
external: nodeBuiltins.map(m => `node:${m}`).concat(nodeBuiltins),
|
||||
sourcemap: !isProduction,
|
||||
minify: isProduction,
|
||||
// Resolve workspace libraries from tsconfig paths
|
||||
alias: {
|
||||
'shared-interfaces': path.join(__dirname, '../../libs/shared/interfaces/src/index.ts'),
|
||||
},
|
||||
});
|
||||
|
||||
console.log('✅ Worker built successfully!');
|
||||
} catch (error) {
|
||||
console.error('❌ Worker build failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
buildWorker();
|
||||
@@ -4,8 +4,16 @@
|
||||
"sourceRoot": "apps/electron-backend/src",
|
||||
"projectType": "application",
|
||||
"targets": {
|
||||
"build-worker": {
|
||||
"executor": "nx:run-commands",
|
||||
"outputs": ["{workspaceRoot}/dist/apps/electron-backend/workers"],
|
||||
"options": {
|
||||
"command": "node apps/electron-backend/build-worker.js"
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
"electron-backend:build-worker",
|
||||
{
|
||||
"projects": ["web", "remote-control-web"],
|
||||
"target": "build"
|
||||
@@ -17,20 +25,7 @@
|
||||
"outputPath": "dist/apps/electron-backend",
|
||||
"main": "apps/electron-backend/src/main.ts",
|
||||
"tsConfig": "apps/electron-backend/tsconfig.app.json",
|
||||
"assets": [
|
||||
"apps/electron-backend/src/assets",
|
||||
{
|
||||
"input": "apps/electron-backend/src/app/workers",
|
||||
"glob": "*.worker.ts",
|
||||
"output": "workers"
|
||||
}
|
||||
],
|
||||
"additionalEntryPoints": [
|
||||
{
|
||||
"entryName": "workers/epg-parser.worker",
|
||||
"entryPath": "apps/electron-backend/src/app/workers/epg-parser.worker.ts"
|
||||
}
|
||||
]
|
||||
"assets": ["apps/electron-backend/src/assets"]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
@@ -54,7 +49,6 @@
|
||||
"nx run electron-backend:serve-electron"
|
||||
]
|
||||
}
|
||||
|
||||
},
|
||||
"serve-electron": {
|
||||
"executor": "nx-electron:execute",
|
||||
|
||||
@@ -24,8 +24,6 @@ export default class EpgEvents {
|
||||
* Bootstrap EPG events
|
||||
*/
|
||||
static bootstrapEpgEvents(): Electron.IpcMain {
|
||||
console.log('[EPG Events] Bootstrapping EPG events...');
|
||||
|
||||
// Fetch EPG from URLs
|
||||
ipcMain.handle('FETCH_EPG', async (event, args: { url: string[] }) => {
|
||||
return await this.handleFetchEpg(args.url);
|
||||
@@ -79,7 +77,7 @@ export default class EpgEvents {
|
||||
await Promise.all(promises);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('[EPG Events] Error fetching EPG:', error);
|
||||
console.error(this.loggerLabel, 'Error fetching EPG:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
@@ -93,21 +91,19 @@ export default class EpgEvents {
|
||||
private static async fetchEpgFromUrl(url: string): Promise<void> {
|
||||
// Skip if already fetched
|
||||
if (this.fetchedUrls.has(url)) {
|
||||
console.log(this.loggerLabel, 'URL already fetched:', url);
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Get the proper worker path following Electron's recommended approach
|
||||
// https://www.electronjs.org/docs/latest/tutorial/multithreading
|
||||
let workerPath: string;
|
||||
|
||||
|
||||
if (app.isPackaged) {
|
||||
// In packaged app: Resources/app.asar.unpacked/electron-backend/workers/epg-parser.worker.js
|
||||
const appPath = app.getAppPath();
|
||||
const unpackedPath = appPath.replace('app.asar', 'app.asar.unpacked');
|
||||
// In packaged app: Resources/dist/apps/electron-backend/workers/epg-parser.worker.js
|
||||
const resourcesPath = path.dirname(app.getAppPath());
|
||||
workerPath = path.join(
|
||||
unpackedPath,
|
||||
resourcesPath,
|
||||
'dist',
|
||||
'apps',
|
||||
'electron-backend',
|
||||
'workers',
|
||||
'epg-parser.worker.js'
|
||||
@@ -117,17 +113,11 @@ export default class EpgEvents {
|
||||
workerPath = path.join(__dirname, 'workers', 'epg-parser.worker.js');
|
||||
}
|
||||
|
||||
console.log(this.loggerLabel, 'Creating worker for:', url);
|
||||
console.log(this.loggerLabel, 'Worker path:', workerPath);
|
||||
console.log(this.loggerLabel, 'App path:', app.getAppPath());
|
||||
console.log(this.loggerLabel, 'Is packaged:', app.isPackaged);
|
||||
|
||||
let worker: Worker;
|
||||
try {
|
||||
// Worker threads require file:// URLs wrapped in URL object for packaged apps
|
||||
const workerURL = pathToFileURL(workerPath);
|
||||
worker = new Worker(workerURL);
|
||||
console.log(this.loggerLabel, 'Worker created successfully');
|
||||
} catch (error) {
|
||||
console.error(
|
||||
this.loggerLabel,
|
||||
@@ -149,16 +139,8 @@ export default class EpgEvents {
|
||||
url?: string;
|
||||
}) => {
|
||||
if (message.type === 'READY') {
|
||||
console.log(
|
||||
this.loggerLabel,
|
||||
'Worker ready, sending fetch command'
|
||||
);
|
||||
worker.postMessage({ type: 'FETCH_EPG', url });
|
||||
} else if (message.type === 'EPG_PARSED') {
|
||||
console.log(
|
||||
this.loggerLabel,
|
||||
'EPG parsed in worker, merging data...'
|
||||
);
|
||||
if (message.data) {
|
||||
this.mergeEpgData(message.data);
|
||||
this.fetchedUrls.add(url);
|
||||
@@ -201,11 +183,6 @@ export default class EpgEvents {
|
||||
* Merge EPG data (optimized for large datasets)
|
||||
*/
|
||||
private static mergeEpgData(newData: EpgData): void {
|
||||
console.log(
|
||||
this.loggerLabel,
|
||||
`Merging ${newData.channels.length} channels and ${newData.programs.length} programs...`
|
||||
);
|
||||
|
||||
// Merge channels (avoid duplicates)
|
||||
const existingChannelIds = new Set(
|
||||
this.epgData.channels.map((c) => c.id)
|
||||
@@ -226,11 +203,6 @@ export default class EpgEvents {
|
||||
|
||||
// Rebuild merged data structure
|
||||
this.rebuildMergedData();
|
||||
|
||||
console.log(
|
||||
this.loggerLabel,
|
||||
`Merged EPG data: ${this.epgData.channels.length} channels, ${this.epgData.programs.length} programs`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -238,7 +210,6 @@ export default class EpgEvents {
|
||||
* Optimized for large datasets
|
||||
*/
|
||||
private static rebuildMergedData(): void {
|
||||
console.log(this.loggerLabel, 'Rebuilding merged data structure...');
|
||||
this.epgDataMerged.clear();
|
||||
|
||||
// Create channel lookup map for O(1) access
|
||||
@@ -265,31 +236,17 @@ export default class EpgEvents {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
this.loggerLabel,
|
||||
`Rebuild complete. ${this.epgDataMerged.size} channels with programs`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get programs for a specific channel
|
||||
*/
|
||||
private static handleGetChannelPrograms(channelId: string): EpgProgram[] {
|
||||
console.log('[EPG Events] Getting programs for channel:', channelId);
|
||||
console.log(
|
||||
'[EPG Events] Total merged channels:',
|
||||
this.epgDataMerged.size
|
||||
);
|
||||
|
||||
// First try exact ID match
|
||||
let channelData = this.epgDataMerged.get(channelId);
|
||||
|
||||
// If not found, try to find by display name
|
||||
if (!channelData) {
|
||||
console.log(
|
||||
'[EPG Events] No exact match, searching by display name...'
|
||||
);
|
||||
for (const [id, channel] of this.epgDataMerged.entries()) {
|
||||
const displayNames = channel.displayName.map((d) =>
|
||||
d.value.toLowerCase()
|
||||
@@ -302,22 +259,12 @@ export default class EpgEvents {
|
||||
channelId.toLowerCase().includes(name)
|
||||
)
|
||||
) {
|
||||
console.log(
|
||||
'[EPG Events] Found match by name:',
|
||||
id,
|
||||
'→',
|
||||
channel.displayName[0]?.value
|
||||
);
|
||||
channelData = channel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
'[EPG Events] Found programs:',
|
||||
channelData?.programs?.length || 0
|
||||
);
|
||||
return channelData?.programs || [];
|
||||
}
|
||||
|
||||
@@ -343,7 +290,5 @@ export default class EpgEvents {
|
||||
// Terminate all workers
|
||||
this.workers.forEach((worker) => worker.terminate());
|
||||
this.workers.clear();
|
||||
|
||||
console.log(this.loggerLabel, 'EPG data cleared');
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import axios from 'axios';
|
||||
import { parse as parseEpg } from 'epg-parser';
|
||||
import { EpgData, EpgProgram } from 'shared-interfaces';
|
||||
import { parentPort } from 'worker_threads';
|
||||
@@ -29,32 +28,28 @@ const loggerLabel = '[EPG Worker]';
|
||||
*/
|
||||
async function fetchAndParseEpg(url: string): Promise<EpgData> {
|
||||
try {
|
||||
console.log(loggerLabel, 'Fetching EPG from:', url);
|
||||
|
||||
const isGzipped = url.endsWith('.gz');
|
||||
const axiosConfig = isGzipped
|
||||
? { responseType: 'arraybuffer' as const }
|
||||
: {};
|
||||
|
||||
const response = await axios.get(url.trim(), axiosConfig);
|
||||
console.log(
|
||||
loggerLabel,
|
||||
'EPG data fetched, size:',
|
||||
response.data.length
|
||||
);
|
||||
// Use native fetch (available in Node.js 18+)
|
||||
const response = await fetch(url.trim());
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
let xmlString: string;
|
||||
|
||||
if (isGzipped) {
|
||||
console.log(loggerLabel, 'Unzipping...');
|
||||
const buffer = Buffer.from(response.data);
|
||||
// For gzipped files, get as ArrayBuffer
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
const decompressed = gunzipSync(buffer);
|
||||
xmlString = decompressed.toString('utf-8');
|
||||
} else {
|
||||
xmlString = response.data;
|
||||
// For regular files, get as text
|
||||
xmlString = await response.text();
|
||||
}
|
||||
|
||||
console.log(loggerLabel, 'Parsing EPG XML...');
|
||||
const parsed = parseEpg(xmlString);
|
||||
|
||||
// Normalize data to match our interface
|
||||
@@ -89,11 +84,6 @@ async function fetchAndParseEpg(url: string): Promise<EpgData> {
|
||||
) as EpgProgram[],
|
||||
};
|
||||
|
||||
console.log(
|
||||
loggerLabel,
|
||||
`Parsed ${normalized.channels.length} channels and ${normalized.programs.length} programs`
|
||||
);
|
||||
|
||||
return normalized;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
@@ -109,20 +99,12 @@ async function fetchAndParseEpg(url: string): Promise<EpgData> {
|
||||
* Worker message handler
|
||||
*/
|
||||
if (parentPort) {
|
||||
console.log(loggerLabel, 'Worker initialized and ready');
|
||||
|
||||
parentPort.on('message', async (message: WorkerMessage) => {
|
||||
console.log(loggerLabel, 'Received message:', message.type);
|
||||
try {
|
||||
if (
|
||||
message.type === 'FETCH_EPG' ||
|
||||
message.type === 'FORCE_FETCH'
|
||||
) {
|
||||
console.log(
|
||||
loggerLabel,
|
||||
'Starting EPG fetch for:',
|
||||
message.url
|
||||
);
|
||||
const parsedData = await fetchAndParseEpg(message.url);
|
||||
|
||||
const response: WorkerResponse = {
|
||||
@@ -131,10 +113,6 @@ if (parentPort) {
|
||||
url: message.url,
|
||||
};
|
||||
|
||||
console.log(
|
||||
loggerLabel,
|
||||
'Sending parsed data back to main thread'
|
||||
);
|
||||
parentPort?.postMessage(response);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -150,7 +128,6 @@ if (parentPort) {
|
||||
});
|
||||
|
||||
// Notify parent that worker is ready
|
||||
console.log(loggerLabel, 'Sending READY message');
|
||||
parentPort.postMessage({ type: 'READY' });
|
||||
} else {
|
||||
console.error(loggerLabel, 'parentPort is not available!');
|
||||
|
||||
Reference in new issue
Block a user