/** * Database service for Electron renderer process * Communicates with the main process database via IPC */ import { Injectable } from '@angular/core'; import { PlaylistMeta, XtreamCategory, } from 'shared-interfaces'; export interface XCategoryFromDb { id: number; name: string; playlist_id: string; type: 'movies' | 'live' | 'series'; xtream_id: number; hidden: boolean; } export interface XtreamContent { id: number; category_id: number; title: string; rating: string; added: string; poster_url: string; epg_channel_id?: string | null; tv_archive?: number | null; tv_archive_duration?: number | null; direct_source?: string | null; xtream_id: number; type: string; added_at?: string; viewed_at?: string; } export interface XtreamPlaylist { id: string; name: string; serverUrl: string; username: string; password: string; type: string; } type XtreamDatabasePlaylistUpdate = { name?: string; username?: string; password?: string; serverUrl?: string; lastUpdated?: string; }; type XtreamContentStream = | { category_id: string | number; rating?: string | number; rating_imdb?: string; last_modified?: string; added?: string; stream_icon?: string; poster?: string; cover?: string; name?: string; title?: string; epg_channel_id?: string; tv_archive?: string | number; tv_archive_duration?: string | number; direct_source?: string; series_id?: string | number; stream_id?: string | number; } | Record; export interface GlobalSearchResult extends XtreamContent { playlist_id: string; playlist_name: string; } export interface GlobalRecentItem extends XtreamContent { playlist_id: string; playlist_name: string; viewed_at: string; } export interface GlobalFavoriteItem extends XtreamContent { playlist_id: string; playlist_name: string; added_at: string; } export interface DbOperationEvent { operationId?: string; operation: string; playlistId?: string; status: 'started' | 'progress' | 'completed' | 'cancelled' | 'error'; phase?: string; current?: number; total?: number; increment?: number; error?: string; } export interface DbOperationOptions { operationId?: string; onEvent?: (event: DbOperationEvent) => void; } export type XtreamImportStatus = | 'idle' | 'importing' | 'completed' | 'cancelled' | 'failed'; export function isDbAbortError(error: unknown): boolean { return error instanceof Error && error.name === 'AbortError'; } export type GlobalRecentlyAddedKind = 'all' | 'vod' | 'series'; export interface GlobalRecentlyAddedItem extends XtreamContent { playlist_id: string; playlist_name: string; added_at: string; } @Injectable({ providedIn: 'root', }) export class DatabaseService { private buildXtreamImportStateKey( playlistId: string, type: 'live' | 'movie' | 'series' ): string { return `xtream-import-status:${playlistId}:${type}`; } createOperationId(prefix = 'db-op'): string { return ( globalThis.crypto?.randomUUID?.() ?? `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}` ); } supportsDbOperationEvents(): boolean { return typeof window.electron.onDbOperationEvent === 'function'; } supportsDbOperationCancellation(): boolean { return typeof window.electron.dbCancelOperation === 'function'; } async cancelOperation(operationId: string): Promise { if (!operationId || !this.supportsDbOperationCancellation()) { return false; } try { const result = await window.electron.dbCancelOperation(operationId); return result.success; } catch (error) { console.error('Error cancelling DB operation:', error); return false; } } private subscribeToOperation( operationId: string, operation: string, onEvent?: (event: DbOperationEvent) => void, onProgress?: (count: number) => void ): (() => void) | undefined { if (!onEvent && !onProgress) { return undefined; } if (window.electron.onDbOperationEvent) { return window.electron.onDbOperationEvent( (event: DbOperationEvent) => { if ( event.operationId !== operationId || event.operation !== operation ) { return; } onEvent?.(event); if (event.status === 'progress') { onProgress?.(event.increment ?? event.current ?? 0); } } ); } if (onProgress && operation === 'save-content') { window.electron.onDbSaveContentProgress(onProgress); return () => window.electron.removeDbSaveContentProgress(); } return undefined; } private async runWithOperationEvents( prefix: string, operation: string, execute: (operationId: string) => Promise, options?: DbOperationOptions, onProgress?: (count: number) => void ): Promise { const operationId = options?.operationId ?? this.createOperationId(prefix); const unsubscribe = this.subscribeToOperation( operationId, operation, options?.onEvent, onProgress ); try { return await execute(operationId); } finally { unsubscribe?.(); } } /** * Delete a playlist and all its related data */ async deletePlaylist( playlistId: string, options?: DbOperationOptions ): Promise { try { await this.runWithOperationEvents( 'db-delete-playlist', 'delete-playlist', (operationId) => window.electron.dbDeletePlaylist(playlistId, operationId), options ); return true; } catch (error) { if (!isDbAbortError(error)) { console.error('Error deleting playlist:', error); } return false; } } /** * Delete all content and categories for an Xtream playlist (for refresh) * Keeps the playlist entry but removes all imported data * Returns saved favorites, recently viewed xtreamIds, and hidden categories for restoration */ async deleteXtreamPlaylistContent( playlistId: string, options?: DbOperationOptions ): Promise<{ success: boolean; favoritedXtreamIds: number[]; recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[]; hiddenCategories: { xtreamId: number; type: string }[]; }> { return this.runWithOperationEvents( 'db-delete-xtream-content', 'delete-xtream-content', (operationId) => window.electron.dbDeleteXtreamContent(playlistId, operationId), options ); } /** * Restore favorites and recently viewed items after Xtream refresh */ async restoreXtreamUserData( playlistId: string, favoritedXtreamIds: number[], recentlyViewedXtreamIds: { xtreamId: number; viewedAt: string }[], options?: DbOperationOptions ): Promise { await this.runWithOperationEvents( 'db-restore-xtream-user-data', 'restore-xtream-user-data', (operationId) => window.electron.dbRestoreXtreamUserData( playlistId, favoritedXtreamIds, recentlyViewedXtreamIds, operationId ), options ); } async deleteAllPlaylists(options?: DbOperationOptions): Promise { try { await this.runWithOperationEvents( 'db-delete-all-playlists', 'delete-all-playlists', (operationId) => window.electron.dbDeleteAllPlaylists(operationId), options ); return true; } catch (error) { if (!isDbAbortError(error)) { console.error('Error deleting all playlists:', error); } return false; } } /** * Update playlist basic info */ async updateXtreamPlaylist( playlist: Pick ): Promise { try { await window.electron.dbUpdatePlaylist(playlist.id, { name: playlist.name, }); return true; } catch (error) { console.error('Error updating playlist:', error); return false; } } /** * Update playlist details including credentials */ async updateXtreamPlaylistDetails(playlist: { id: string; title?: string; username?: string; password?: string; serverUrl?: string; updateDate?: number; }): Promise { try { const updates: XtreamDatabasePlaylistUpdate = {}; if (playlist.title) updates.name = playlist.title; if (playlist.username) updates.username = playlist.username; if (playlist.password) updates.password = playlist.password; if (playlist.serverUrl) updates.serverUrl = playlist.serverUrl; if (playlist.updateDate !== undefined) updates.lastUpdated = new Date( playlist.updateDate ).toISOString(); await window.electron.dbUpdatePlaylist(playlist.id, updates); return true; } catch (error) { console.error('Error updating playlist details:', error); return false; } } /** * Check if categories exist */ async hasXtreamCategories( playlistId: string, type: 'live' | 'movies' | 'series' ): Promise { return await window.electron.dbHasCategories(playlistId, type); } /** * Get categories for a playlist */ async getXtreamCategories( playlistId: string, type: 'live' | 'movies' | 'series' ): Promise { return await window.electron.dbGetCategories(playlistId, type); } /** * Save categories in bulk * Optionally accepts hidden category xtreamIds to restore visibility preferences */ async saveXtreamCategories( playlistId: string, categories: XtreamCategory[], type: 'live' | 'movies' | 'series', hiddenCategoryXtreamIds?: number[] ): Promise { await window.electron.dbSaveCategories( playlistId, categories, type, hiddenCategoryXtreamIds ); } /** * Get all categories for a playlist (including hidden, for management dialog) */ async getAllXtreamCategories( playlistId: string, type: 'live' | 'movies' | 'series' ): Promise { return await window.electron.dbGetAllCategories(playlistId, type); } /** * Update category visibility (show/hide categories) */ async updateCategoryVisibility( categoryIds: number[], hidden: boolean ): Promise { try { await window.electron.dbUpdateCategoryVisibility( categoryIds, hidden ); return true; } catch (error) { console.error('Error updating category visibility:', error); return false; } } /** * Check if content exists */ async hasXtreamContent( playlistId: string, type: 'live' | 'movie' | 'series' ): Promise { return await window.electron.dbHasContent(playlistId, type); } /** * Get content for a playlist */ async getXtreamContent( playlistId: string, type: 'live' | 'movie' | 'series' ): Promise { return await window.electron.dbGetContent(playlistId, type); } /** * Save content in bulk */ async saveXtreamContent( playlistId: string, streams: XtreamContentStream[], type: 'live' | 'movie' | 'series', onProgress?: (count: number) => void, options?: DbOperationOptions ): Promise { try { const result = await this.runWithOperationEvents( 'db-save-content', 'save-content', (operationId) => window.electron.dbSaveContent( playlistId, streams, type, operationId ), options, onProgress ); return result.count; } catch (error) { if (!isDbAbortError(error)) { console.error('Error saving Xtream content:', error); } throw error; } } async clearXtreamImportCache( playlistId: string, type: 'live' | 'movie' | 'series' ): Promise { try { await window.electron.dbClearXtreamImportCache(playlistId, type); return true; } catch (error) { console.error('Error clearing Xtream import cache:', error); return false; } } async getAppState(key: string): Promise { try { return await window.electron.dbGetAppState(key); } catch (error) { console.error('Error getting app state:', error); return null; } } async setAppState(key: string, value: string): Promise { try { await window.electron.dbSetAppState(key, value); return true; } catch (error) { console.error('Error setting app state:', error); return false; } } async getXtreamImportStatus( playlistId: string, type: 'live' | 'movie' | 'series' ): Promise { const value = await this.getAppState( this.buildXtreamImportStateKey(playlistId, type) ); switch (value) { case 'importing': case 'completed': case 'cancelled': case 'failed': case 'idle': return value; default: return 'idle'; } } async setXtreamImportStatus( playlistId: string, type: 'live' | 'movie' | 'series', status: XtreamImportStatus ): Promise { return this.setAppState( this.buildXtreamImportStateKey(playlistId, type), status ); } /** * Search content within a playlist */ async searchXtreamContent( playlistId: string, searchTerm: string, types: string[], excludeHidden?: boolean ): Promise { return await window.electron.dbSearchContent( playlistId, searchTerm, types, excludeHidden ); } /** * Global search across all playlists */ async globalSearchContent( searchTerm: string, types: string[], excludeHidden?: boolean ): Promise { return await window.electron.dbGlobalSearch(searchTerm, types, excludeHidden); } /** * Get recently added VOD and series items across all Xtream playlists. */ async getGlobalRecentlyAdded( kind: GlobalRecentlyAddedKind, limit = 200 ): Promise { try { const items = await window.electron.dbGetGlobalRecentlyAdded( kind, limit ); return items || []; } catch (error) { console.error('Error getting global recently added items:', error); return []; } } /** * Get recently viewed items */ async getGlobalRecentlyViewed(): Promise { try { const items = await window.electron.dbGetRecentlyViewed(); return items || []; } catch (error) { console.error('Error getting recently viewed:', error); return []; } } /** * Get global favorites across all playlists */ async getGlobalFavorites(): Promise { try { const items = await window.electron.dbGetGlobalFavorites(); return items || []; } catch (error) { console.error('Error getting global favorites:', error); return []; } } /** * Get global favorites across all playlists (all content types) */ async getAllGlobalFavorites(): Promise { try { const items = await window.electron.dbGetAllGlobalFavorites(); return items || []; } catch (error) { console.error('Error getting all global favorites:', error); return []; } } /** * Clear recently viewed items */ async clearGlobalRecentlyViewed(): Promise { try { await window.electron.dbClearRecentlyViewed(); } catch (error) { console.error('Error clearing recently viewed:', error); throw error; } } /** * Get playlist by ID */ async getPlaylistById(playlistId: string): Promise { return await window.electron.dbGetPlaylist(playlistId); } /** * Create a new playlist */ async createPlaylist(playlist: PlaylistMeta): Promise { await window.electron.dbCreatePlaylist({ id: playlist._id, name: playlist.title, serverUrl: playlist.serverUrl, username: playlist.username, password: playlist.password, type: 'xtream', }); } /** * Add content to favorites */ async addToFavorites( contentId: number, playlistId: string ): Promise { try { await window.electron.dbAddFavorite(contentId, playlistId); return true; } catch (error) { console.error('Error adding to favorites:', error); return false; } } /** * Remove content from favorites */ async removeFromFavorites( contentId: number, playlistId: string ): Promise { try { await window.electron.dbRemoveFavorite(contentId, playlistId); return true; } catch (error) { console.error('Error removing from favorites:', error); return false; } } /** * Check if content is favorited */ async isFavorite(contentId: number, playlistId: string): Promise { try { return await window.electron.dbIsFavorite(contentId, playlistId); } catch (error) { console.error('Error checking favorite:', error); return false; } } /** * Get all favorites for a playlist */ async getFavorites(playlistId: string): Promise { try { return await window.electron.dbGetFavorites(playlistId); } catch (error) { console.error('Error getting favorites:', error); return []; } } /** * Get recently viewed items for a specific playlist */ async getRecentItems(playlistId: string): Promise { try { return await window.electron.dbGetRecentItems(playlistId); } catch (error) { console.error('Error getting recent items:', error); return []; } } /** * Add item to recently viewed */ async addRecentItem( contentId: number, playlistId: string ): Promise { try { await window.electron.dbAddRecentItem(contentId, playlistId); return true; } catch (error) { console.error('Error adding recent item:', error); return false; } } /** * Clear recently viewed for a specific playlist */ async clearPlaylistRecentItems(playlistId: string): Promise { try { await window.electron.dbClearPlaylistRecentItems(playlistId); return true; } catch (error) { console.error('Error clearing playlist recent items:', error); return false; } } /** * Remove specific item from recently viewed */ async removeRecentItem( contentId: number, playlistId: string ): Promise { try { await window.electron.dbRemoveRecentItem(contentId, playlistId); return true; } catch (error) { console.error('Error removing recent item:', error); return false; } } /** * Get content by xtream ID */ async getContentByXtreamId( xtreamId: number, playlistId: string ): Promise { try { return await window.electron.dbGetContentByXtreamId( xtreamId, playlistId ); } catch (error) { console.error('Error getting content by xtream ID:', error); return null; } } }