diff --git a/apps/electron-backend/src/app/api/main.preload.ts b/apps/electron-backend/src/app/api/main.preload.ts index db4ee78c8..5da932f3b 100644 --- a/apps/electron-backend/src/app/api/main.preload.ts +++ b/apps/electron-backend/src/app/api/main.preload.ts @@ -28,6 +28,16 @@ contextBridge.exposeInMainWorld('electron', { ) => { ipcRenderer.on('EPG_PROGRESS_UPDATE', (_event, data) => callback(data)); }, + // DB save content progress listener + onDbSaveContentProgress: (callback: (count: number) => void) => { + ipcRenderer.on('DB_SAVE_CONTENT_PROGRESS', (_event, count) => + callback(count) + ); + }, + // Remove DB save content progress listener + removeDbSaveContentProgress: () => { + ipcRenderer.removeAllListeners('DB_SAVE_CONTENT_PROGRESS'); + }, getAppVersion: () => ipcRenderer.invoke('get-app-version'), platform: process.platform, fetchPlaylistByUrl: (url: string, title?: string) => diff --git a/apps/electron-backend/src/app/events/database/content.events.ts b/apps/electron-backend/src/app/events/database/content.events.ts index 53baa54d2..571b4392f 100644 --- a/apps/electron-backend/src/app/events/database/content.events.ts +++ b/apps/electron-backend/src/app/events/database/content.events.ts @@ -184,8 +184,8 @@ ipcMain.handle( ); totalInserted += chunk.length; - // Send progress update - event.sender.send('DB_SAVE_CONTENT_PROGRESS', totalInserted); + // Send incremental progress update (chunk size, not cumulative total) + event.sender.send('DB_SAVE_CONTENT_PROGRESS', chunk.length); } return { success: true, count: totalInserted }; diff --git a/apps/web/src/app/xtream-tauri/data-sources/electron-xtream-data-source.ts b/apps/web/src/app/xtream-tauri/data-sources/electron-xtream-data-source.ts index 18b022ef8..521d0d8f3 100644 --- a/apps/web/src/app/xtream-tauri/data-sources/electron-xtream-data-source.ts +++ b/apps/web/src/app/xtream-tauri/data-sources/electron-xtream-data-source.ts @@ -184,7 +184,9 @@ export class ElectronXtreamDataSource implements IXtreamDataSource { async getContent( playlistId: string, credentials: XtreamCredentials, - type: StreamType + type: StreamType, + onProgress?: (count: number) => void, + onTotal?: (total: number) => void ): Promise { // Check if we have cached data const exists = await this.dbService.hasXtreamContent(playlistId, type); @@ -198,11 +200,17 @@ export class ElectronXtreamDataSource implements IXtreamDataSource { const remoteData = await this.apiService.getStreams(credentials, type); if (remoteData && Array.isArray(remoteData) && remoteData.length > 0) { - // Save to cache + // Report total items to import + if (onTotal) { + onTotal(remoteData.length); + } + + // Save to cache with progress tracking await this.dbService.saveXtreamContent( playlistId, remoteData as any[], - type + type, + onProgress ); } diff --git a/apps/web/src/app/xtream-tauri/data-sources/pwa-xtream-data-source.ts b/apps/web/src/app/xtream-tauri/data-sources/pwa-xtream-data-source.ts index 20f82bae1..dc8b37136 100644 --- a/apps/web/src/app/xtream-tauri/data-sources/pwa-xtream-data-source.ts +++ b/apps/web/src/app/xtream-tauri/data-sources/pwa-xtream-data-source.ts @@ -167,7 +167,9 @@ export class PwaXtreamDataSource implements IXtreamDataSource { async getContent( playlistId: string, credentials: XtreamCredentials, - type: StreamType + type: StreamType, + onProgress?: (count: number) => void, + onTotal?: (total: number) => void ): Promise { const cacheKey = `${playlistId}-${type}-content`; @@ -179,6 +181,14 @@ export class PwaXtreamDataSource implements IXtreamDataSource { // Fetch from API const content = await this.apiService.getStreams(credentials, type); + // Report total and progress (PWA doesn't have incremental save, so report all at once) + if (onTotal) { + onTotal(content.length); + } + if (onProgress) { + onProgress(content.length); + } + // Cache in memory this.contentCache.set(cacheKey, content); diff --git a/apps/web/src/app/xtream-tauri/data-sources/xtream-data-source.interface.ts b/apps/web/src/app/xtream-tauri/data-sources/xtream-data-source.interface.ts index 9cfd4afb8..9a01c97f8 100644 --- a/apps/web/src/app/xtream-tauri/data-sources/xtream-data-source.interface.ts +++ b/apps/web/src/app/xtream-tauri/data-sources/xtream-data-source.interface.ts @@ -199,11 +199,15 @@ export interface IXtreamDataSource { /** * Get content/streams for a playlist and type + * @param onProgress - Optional callback for import progress (count) + * @param onTotal - Optional callback for total items to import */ getContent( playlistId: string, credentials: XtreamCredentials, - type: StreamType + type: StreamType, + onProgress?: ProgressCallback, + onTotal?: (total: number) => void ): Promise; /** diff --git a/apps/web/src/app/xtream-tauri/stores/features/with-content.feature.ts b/apps/web/src/app/xtream-tauri/stores/features/with-content.feature.ts index 6170b033d..6e5921d01 100644 --- a/apps/web/src/app/xtream-tauri/stores/features/with-content.feature.ts +++ b/apps/web/src/app/xtream-tauri/stores/features/with-content.feature.ts @@ -181,7 +181,7 @@ export function withContent() { }, /** - * Fetch all content/streams in parallel + * Fetch all content/streams in parallel with progress tracking */ async fetchAllContent(): Promise { const ctx = getCredentialsFromStore(); @@ -189,22 +189,42 @@ export function withContent() { patchState(store, { isLoadingContent: true }); + // Track combined progress across all content types + let totalItems = 0; + let importedItems = 0; + + const onTotal = (count: number) => { + totalItems += count; + patchState(store, { itemsToImport: totalItems }); + }; + + const onProgress = (count: number) => { + importedItems += count; + patchState(store, { importCount: importedItems }); + }; + try { const [live, vod, series] = await Promise.all([ dataSource.getContent( ctx.playlistId, ctx.credentials, - 'live' + 'live', + onProgress, + onTotal ), dataSource.getContent( ctx.playlistId, ctx.credentials, - 'movie' + 'movie', + onProgress, + onTotal ), dataSource.getContent( ctx.playlistId, ctx.credentials, - 'series' + 'series', + onProgress, + onTotal ), ]); @@ -227,13 +247,13 @@ export function withContent() { const ctx = getCredentialsFromStore(); if (!ctx) return; - patchState(store, { isImporting: true, importCount: 0 }); + patchState(store, { isImporting: true, importCount: 0, itemsToImport: 0 }); try { // Fetch categories first await this.fetchAllCategories(); - // Then fetch content + // Then fetch content (with progress tracking) await this.fetchAllContent(); // Restore user data if needed diff --git a/global.d.ts b/global.d.ts index 5efe28ee2..793ef6ce8 100644 --- a/global.d.ts +++ b/global.d.ts @@ -210,6 +210,9 @@ declare global { error?: string; }) => void ) => void; + // DB save content progress listener + onDbSaveContentProgress: (callback: (count: number) => void) => void; + removeDbSaveContentProgress: () => void; dbDeleteAllPlaylists: () => Promise<{ success: boolean }>; }; process: NodeJS.Process; diff --git a/libs/services/src/lib/database-electron.service.ts b/libs/services/src/lib/database-electron.service.ts index e51f8498d..929ef648b 100644 --- a/libs/services/src/lib/database-electron.service.ts +++ b/libs/services/src/lib/database-electron.service.ts @@ -237,19 +237,22 @@ export class DatabaseService { ): Promise { // Setup progress listener if callback provided if (onProgress) { - window.addEventListener('message', (event) => { - if (event.data.type === 'DB_SAVE_CONTENT_PROGRESS') { - onProgress(event.data.count); - } - }); + window.electron.onDbSaveContentProgress(onProgress); } - const result = await window.electron.dbSaveContent( - playlistId, - streams, - type - ); - return result.count; + try { + const result = await window.electron.dbSaveContent( + playlistId, + streams, + type + ); + return result.count; + } finally { + // Clean up the listener + if (onProgress) { + window.electron.removeDbSaveContentProgress(); + } + } } /**