diff --git a/apps/electron-backend/src/app/api/main.preload.ts b/apps/electron-backend/src/app/api/main.preload.ts index 117ace3dd..c2cc0759e 100644 --- a/apps/electron-backend/src/app/api/main.preload.ts +++ b/apps/electron-backend/src/app/api/main.preload.ts @@ -184,10 +184,11 @@ contextBridge.exposeInMainWorld('electron', { dbSearchContent: ( playlistId: string, searchTerm: string, - types: string[] - ) => ipcRenderer.invoke('DB_SEARCH_CONTENT', playlistId, searchTerm, types), - dbGlobalSearch: (searchTerm: string, types: string[]) => - ipcRenderer.invoke('DB_GLOBAL_SEARCH', searchTerm, types), + types: string[], + excludeHidden?: boolean + ) => ipcRenderer.invoke('DB_SEARCH_CONTENT', playlistId, searchTerm, types, excludeHidden), + dbGlobalSearch: (searchTerm: string, types: string[], excludeHidden?: boolean) => + ipcRenderer.invoke('DB_GLOBAL_SEARCH', searchTerm, types, excludeHidden), dbGetRecentlyViewed: () => ipcRenderer.invoke('DB_GET_RECENTLY_VIEWED'), dbClearRecentlyViewed: () => ipcRenderer.invoke('DB_CLEAR_RECENTLY_VIEWED'), // Favorites 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 e863a65e7..2f0e3d1d7 100644 --- a/apps/electron-backend/src/app/events/database/content.events.ts +++ b/apps/electron-backend/src/app/events/database/content.events.ts @@ -259,8 +259,9 @@ ipcMain.handle( */ ipcMain.handle( 'DB_SEARCH_CONTENT', - async (event, playlistId: string, searchTerm: string, types: string[]) => { + async (event, playlistId: string, searchTerm: string, types: string[], excludeHidden = false) => { try { + if (!types || types.length === 0) return []; const db = await getDatabase(); const searchTermLower = searchTerm.toLocaleLowerCase(); const likePatterns = buildLikePatterns(searchTerm); @@ -270,6 +271,19 @@ ipcMain.handle( // Pre-filter with SQL LIKE (case-insensitive for ASCII, handled in native C code). // This avoids loading the entire content table into JS memory. + const conditions = [ + eq(schema.categories.playlistId, playlistId), + inArray( + schema.content.type, + types as Array<'live' | 'movie' | 'series'> + ), + or(...likeConditions), + ]; + + if (excludeHidden) { + conditions.push(eq(schema.categories.hidden, false)); + } + const candidates = await db .select({ id: schema.content.id, @@ -286,16 +300,7 @@ ipcMain.handle( schema.categories, eq(schema.content.categoryId, schema.categories.id) ) - .where( - and( - eq(schema.categories.playlistId, playlistId), - inArray( - schema.content.type, - types as Array<'live' | 'movie' | 'series'> - ), - or(...likeConditions) - ) - ) + .where(and(...conditions)) .limit(200); // Post-filter for proper Unicode case-insensitive matching @@ -319,8 +324,9 @@ ipcMain.handle( */ ipcMain.handle( 'DB_GLOBAL_SEARCH', - async (event, searchTerm: string, types: string[]) => { + async (event, searchTerm: string, types: string[], excludeHidden = false) => { try { + if (!types || types.length === 0) return []; const db = await getDatabase(); const searchTermLower = searchTerm.toLocaleLowerCase(); const likePatterns = buildLikePatterns(searchTerm); @@ -332,6 +338,18 @@ ipcMain.handle( // This avoids loading the entire content table into JS memory. // Previously this query loaded ALL content rows (potentially 100k+), // created JS objects for each, then filtered in JS — blocking the main process. + const conditions = [ + inArray( + schema.content.type, + types as Array<'live' | 'movie' | 'series'> + ), + or(...likeConditions), + ]; + + if (excludeHidden) { + conditions.push(eq(schema.categories.hidden, false)); + } + const candidates = await db .select({ id: schema.content.id, @@ -354,15 +372,7 @@ ipcMain.handle( schema.playlists, eq(schema.categories.playlistId, schema.playlists.id) ) - .where( - and( - inArray( - schema.content.type, - types as Array<'live' | 'movie' | 'series'> - ), - or(...likeConditions) - ) - ) + .where(and(...conditions)) .orderBy(schema.content.title) .limit(200); diff --git a/apps/electron-backend/src/app/events/epg.events.ts b/apps/electron-backend/src/app/events/epg.events.ts index ad53ee5db..1aa7fbd97 100644 --- a/apps/electron-backend/src/app/events/epg.events.ts +++ b/apps/electron-backend/src/app/events/epg.events.ts @@ -318,10 +318,6 @@ export default class EpgEvents { case 'EPG_PROGRESS': if (message.stats) { - console.log( - this.loggerLabel, - `Progress: ${message.stats.totalChannels} channels, ${message.stats.totalPrograms} programs` - ); // Forward progress to renderer this.sendProgressToRenderer( url, 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 d3044a5d8..dbb6f292f 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 @@ -240,9 +240,10 @@ export class ElectronXtreamDataSource implements IXtreamDataSource { async searchContent( playlistId: string, searchTerm: string, - types: string[] + types: string[], + excludeHidden?: boolean ): Promise { - return this.dbService.searchXtreamContent(playlistId, searchTerm, types); + return this.dbService.searchXtreamContent(playlistId, searchTerm, types, excludeHidden); } // ========================================================================= 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 8d49d8a46..9fa5ab037 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 @@ -221,7 +221,8 @@ export class PwaXtreamDataSource implements IXtreamDataSource { async searchContent( playlistId: string, searchTerm: string, - types: string[] + types: string[], + _excludeHidden?: boolean ): Promise { const results: any[] = []; const searchLower = searchTerm.toLowerCase(); 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 fa90cc7ae..1e778e9a2 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 @@ -234,7 +234,8 @@ export interface IXtreamDataSource { searchContent( playlistId: string, searchTerm: string, - types: string[] + types: string[], + excludeHidden?: boolean ): Promise; // ========================================================================= diff --git a/apps/web/src/app/xtream-tauri/search-results/search-results.component.html b/apps/web/src/app/xtream-tauri/search-results/search-results.component.html index bd548cef8..f911b1e15 100644 --- a/apps/web/src/app/xtream-tauri/search-results/search-results.component.html +++ b/apps/web/src/app/xtream-tauri/search-results/search-results.component.html @@ -19,6 +19,15 @@ } + + + {{ 'PORTALS.SEARCH_VIEW.VISIBLE_CATEGORIES_ONLY' | translate }} + + + @if (isGlobalSearch) {
diff --git a/apps/web/src/app/xtream-tauri/search-results/search-results.component.ts b/apps/web/src/app/xtream-tauri/search-results/search-results.component.ts index c1387c6cb..84e6df502 100644 --- a/apps/web/src/app/xtream-tauri/search-results/search-results.component.ts +++ b/apps/web/src/app/xtream-tauri/search-results/search-results.component.ts @@ -63,6 +63,7 @@ export class SearchResultsComponent implements AfterViewInit { readonly filters = this.xtreamStore.searchFilters; private static readonly GROUP_BY_STORAGE_KEY = 'global-search-group-by-playlist'; + private static readonly EXCLUDE_HIDDEN_STORAGE_KEY = 'xtream-search-exclude-hidden'; isGlobalSearch = false; @@ -71,6 +72,11 @@ export class SearchResultsComponent implements AfterViewInit { localStorage.getItem(SearchResultsComponent.GROUP_BY_STORAGE_KEY) !== 'false' ); + /** Whether to exclude content from hidden categories */ + readonly excludeHidden = signal( + localStorage.getItem(SearchResultsComponent.EXCLUDE_HIDDEN_STORAGE_KEY) === 'true' + ); + readonly filterConfig = [ { key: 'live' as keyof SearchFilters, @@ -125,13 +131,15 @@ export class SearchResultsComponent implements AfterViewInit { const types = Object.entries(filters) .filter(([_, enabled]) => enabled) .map(([type]) => type); + const excludeHidden = this.excludeHidden(); if (this.isGlobalSearch) { - this.searchGlobal(this.searchTerm(), types); + this.searchGlobal(this.searchTerm(), types, excludeHidden); } else { this.xtreamStore.searchContent({ term: this.searchTerm(), types, + excludeHidden, }); } } @@ -160,12 +168,13 @@ export class SearchResultsComponent implements AfterViewInit { this.xtreamStore.setGlobalSearchResults([]); } - async searchGlobal(term: string, types: string[]) { + async searchGlobal(term: string, types: string[], excludeHidden?: boolean) { this.xtreamStore.setIsSearching(true); try { const results = await this.databaseService.globalSearchContent( term, - types + types, + excludeHidden ); if (results && Array.isArray(results)) { this.xtreamStore.setGlobalSearchResults(results); @@ -211,6 +220,14 @@ export class SearchResultsComponent implements AfterViewInit { localStorage.setItem(SearchResultsComponent.GROUP_BY_STORAGE_KEY, String(value)); } + toggleExcludeHidden(value: boolean) { + this.excludeHidden.set(value); + localStorage.setItem(SearchResultsComponent.EXCLUDE_HIDDEN_STORAGE_KEY, String(value)); + if (this.searchTerm().length >= 3) { + this.executeSearch(); + } + } + getGroupedResults() { return this.groupedResults(); } diff --git a/apps/web/src/app/xtream-tauri/stores/features/with-search.feature.ts b/apps/web/src/app/xtream-tauri/stores/features/with-search.feature.ts index aa11df16e..2d9b1d4dc 100644 --- a/apps/web/src/app/xtream-tauri/stores/features/with-search.feature.ts +++ b/apps/web/src/app/xtream-tauri/stores/features/with-search.feature.ts @@ -70,7 +70,8 @@ export function withSearch() { */ async searchContent( searchTerm: string, - types: string[] + types: string[], + excludeHidden?: boolean ): Promise { // Access parent store's playlistId (from withPortal) const storeAny = store as any; @@ -87,7 +88,8 @@ export function withSearch() { const results = await dataSource.searchContent( playlistId, searchTerm, - types + types, + excludeHidden ); patchState(store, { diff --git a/apps/web/src/app/xtream-tauri/stores/xtream.store.ts b/apps/web/src/app/xtream-tauri/stores/xtream.store.ts index 4837447c7..a7ad98b40 100644 --- a/apps/web/src/app/xtream-tauri/stores/xtream.store.ts +++ b/apps/web/src/app/xtream-tauri/stores/xtream.store.ts @@ -81,7 +81,7 @@ export const XtreamStore = signalStore( const xtreamApiService = inject(XtreamApiService); const ngrxStore = inject(Store); const searchContent = (store as any) - .searchContent as (term: string, types: string[]) => Promise; + .searchContent as (term: string, types: string[], excludeHidden?: boolean) => Promise; return { /** @@ -218,11 +218,11 @@ export const XtreamStore = signalStore( /** * Search content wrapper for rxMethod compatibility - * Can be called with object { term, types } or direct params + * Can be called with object { term, types, excludeHidden } or direct params */ - searchContent(params: { term: string; types: string[] }): void { + searchContent(params: { term: string; types: string[]; excludeHidden?: boolean }): void { // Call the underlying search method from withSearch - void searchContent(params.term, params.types); + void searchContent(params.term, params.types, params.excludeHidden); }, }; }) diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index d05d3a16b..0624c671b 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -446,7 +446,8 @@ "RESULTS_FOUND": "Found {{count}} results", "NO_RESULTS_FOR": "No results found for \"{{term}}\"", "GROUP_BY_PLAYLIST": "Group results by playlist", - "XTREAM_ONLY_NOTE": "Global search is currently available for Xtream playlists only." + "XTREAM_ONLY_NOTE": "Global search is currently available for Xtream playlists only.", + "VISIBLE_CATEGORIES_ONLY": "Visible categories only" }, "STREAM_URL_COPIED": "Stream URL copied to clipboard", "COPY_STREAM_URL": "Copy Stream URL", diff --git a/apps/web/src/typings.d.ts b/apps/web/src/typings.d.ts index aa5b62150..f157308b4 100644 --- a/apps/web/src/typings.d.ts +++ b/apps/web/src/typings.d.ts @@ -150,11 +150,13 @@ declare global { dbSearchContent: ( playlistId: string, searchTerm: string, - types: string[] + types: string[], + excludeHidden?: boolean ) => Promise; dbGlobalSearch: ( searchTerm: string, - types: string[] + types: string[], + excludeHidden?: boolean ) => Promise; dbGetRecentlyViewed: () => Promise; dbClearRecentlyViewed: () => Promise<{ success: boolean }>; diff --git a/global.d.ts b/global.d.ts index 69fbef54f..e0e0ba955 100644 --- a/global.d.ts +++ b/global.d.ts @@ -153,11 +153,13 @@ declare global { dbSearchContent: ( playlistId: string, searchTerm: string, - types: string[] + types: string[], + excludeHidden?: boolean ) => Promise; dbGlobalSearch: ( searchTerm: string, - types: string[] + types: string[], + excludeHidden?: boolean ) => Promise; dbGetRecentlyViewed: () => Promise; dbClearRecentlyViewed: () => Promise<{ success: boolean }>; diff --git a/libs/services/src/lib/database-electron.service.ts b/libs/services/src/lib/database-electron.service.ts index 929ef648b..39b41c056 100644 --- a/libs/services/src/lib/database-electron.service.ts +++ b/libs/services/src/lib/database-electron.service.ts @@ -261,12 +261,14 @@ export class DatabaseService { async searchXtreamContent( playlistId: string, searchTerm: string, - types: string[] + types: string[], + excludeHidden?: boolean ): Promise { return await window.electron.dbSearchContent( playlistId, searchTerm, - types + types, + excludeHidden ); } @@ -275,9 +277,10 @@ export class DatabaseService { */ async globalSearchContent( searchTerm: string, - types: string[] + types: string[], + excludeHidden?: boolean ): Promise { - return await window.electron.dbGlobalSearch(searchTerm, types); + return await window.electron.dbGlobalSearch(searchTerm, types, excludeHidden); } /**