diff --git a/src/app/services/database.service.ts b/src/app/services/database.service.ts index 4c2c93830..8c905a50f 100644 --- a/src/app/services/database.service.ts +++ b/src/app/services/database.service.ts @@ -30,6 +30,17 @@ export interface XtreamPlaylist { type: string; } +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; +} + @Injectable({ providedIn: 'root', }) @@ -272,6 +283,96 @@ export class DatabaseService { ); } + async globalSearchContent( + searchTerm: string, + types: string[] + ): Promise { + const db = await this.getConnection(); + const placeholders = types.map(() => '?').join(','); + + // Use a materialized subquery for better performance + return await db.select( + ` + WITH filtered_content AS ( + SELECT + c.id, + c.category_id, + c.title, + c.rating, + c.added, + c.poster_url, + c.xtream_id, + c.type, + cat.playlist_id, + p.name as playlist_name + FROM content c + INNER JOIN categories cat ON c.category_id = cat.id + INNER JOIN playlists p ON cat.playlist_id = p.id + WHERE c.type IN (${placeholders}) + ) + SELECT * FROM filtered_content + WHERE LOWER(title) LIKE LOWER(?) + ORDER BY title + LIMIT 50 + `, + [...types, `%${searchTerm}%`] + ); + } + + async getGlobalRecentlyViewed(): Promise { + try { + console.log('Starting getGlobalRecentlyViewed query...'); + const db = await this.getConnection(); + console.log('Got database connection'); + + // Check if table exists + const tableCheck = await db.select(` + SELECT name FROM sqlite_master + WHERE type='table' AND name='recently_viewed' + `); + console.log('Tables check:', tableCheck); + + const items = await db.select(` + SELECT + c.id, + c.category_id, + c.title, + c.rating, + c.added, + c.poster_url, + c.xtream_id, + c.type, + cat.playlist_id, + p.name as playlist_name, + rv.viewed_at + FROM recently_viewed rv + INNER JOIN content c ON rv.content_id = c.id + INNER JOIN categories cat ON c.category_id = cat.id + INNER JOIN playlists p ON cat.playlist_id = p.id + ORDER BY rv.viewed_at DESC + LIMIT 100 + `); + + console.log('Query executed, items found:', items?.length); + console.log('First few items:', items?.slice(0, 3)); + + return items || []; + } catch (error) { + console.error('Detailed error in getGlobalRecentlyViewed:', error); + throw error; // Let's throw the error to see it in the component + } + } + + async clearGlobalRecentlyViewed(): Promise { + try { + const db = await this.getConnection(); + await db.execute('DELETE FROM recently_viewed'); + } catch (error) { + console.error('Error clearing global recently viewed:', error); + throw error; + } + } + async getPlaylistById(playlistId: string): Promise { const db = await this.getConnection(); const results = await db.select( diff --git a/src/app/xtream-tauri/with-recent-items.ts b/src/app/xtream-tauri/with-recent-items.ts index bf0eacb55..f344e81ce 100644 --- a/src/app/xtream-tauri/with-recent-items.ts +++ b/src/app/xtream-tauri/with-recent-items.ts @@ -143,6 +143,28 @@ export const withRecentItems = function () { }) ) ), + async loadGlobalRecentItems() { + try { + const items = await dbService.getGlobalRecentlyViewed(); + patchState(store, { + recentItems: items || [], + }); + } catch (error) { + console.error('Error loading global recent items:', error); + patchState(store, { recentItems: [] }); + } + }, + async clearGlobalRecentlyViewed() { + try { + await dbService.clearGlobalRecentlyViewed(); + patchState(store, { recentItems: [] }); + } catch (error) { + console.error( + 'Error clearing global recently viewed:', + error + ); + } + }, })) ); }; diff --git a/src/app/xtream-tauri/xtream-state.ts b/src/app/xtream-tauri/xtream-state.ts index 197ff6138..9fa0291a1 100644 --- a/src/app/xtream-tauri/xtream-state.ts +++ b/src/app/xtream-tauri/xtream-state.ts @@ -2,6 +2,7 @@ import { XtreamCategory } from '../../../shared/xtream-category.interface'; import { XtreamLiveStream } from '../../../shared/xtream-live-stream.interface'; import { XtreamSerieItem } from '../../../shared/xtream-serie-item.interface'; import { XtreamVodStream } from '../../../shared/xtream-vod-stream.interface'; +import { GlobalSearchResult } from '../services/database.service'; import { EpgItem } from '../xtream/epg-item.interface'; export type ContentType = 'live' | 'vod' | 'series'; @@ -39,4 +40,5 @@ export interface XtreamState { epgItems: EpgItem[]; hideExternalInfoDialog: boolean; portalStatus: PortalStatusType; + globalSearchResults: GlobalSearchResult[]; } diff --git a/src/app/xtream-tauri/xtream.store.ts b/src/app/xtream-tauri/xtream.store.ts index ee2be1b5a..c9935e161 100644 --- a/src/app/xtream-tauri/xtream.store.ts +++ b/src/app/xtream-tauri/xtream.store.ts @@ -53,6 +53,7 @@ const initialState: XtreamState = { hideExternalInfoDialog: localStorage.getItem('hideExternalInfoDialog') === 'true', portalStatus: 'unavailable', + globalSearchResults: [], }; /** to decode epg */ @@ -67,108 +68,105 @@ function b64DecodeUnicode(str: string) { } export const XtreamStore = signalStore( + { providedIn: 'root' }, withState(initialState), withRecentItems(), withFavorites(), - withComputed((store) => { - return { - getCategoriesBySelectedType: computed(() => { - const type = store.selectedContentType(); - return type === 'live' - ? store.liveCategories() - : type === 'vod' - ? store.vodCategories() - : store.serialCategories(); - }), - getSelectedCategory: computed(() => { - const categoryId = store.selectedCategoryId(); - return [ - ...store.vodCategories(), - ...store.liveCategories(), - ...store.serialCategories(), - ].find((c) => c.id === categoryId); - }), - getImportCount: computed(() => store.importCount()), - getSelectedItemById: computed(() => { - const categoryType = store.selectedContentType(); - const content = - categoryType === 'live' - ? store.liveStreams() - : categoryType === 'vod' - ? store.vodStreams() - : store.serialStreams(); + withComputed((store) => ({ + getCategoriesBySelectedType: computed(() => { + const type = store.selectedContentType(); + return type === 'live' + ? store.liveCategories() + : type === 'vod' + ? store.vodCategories() + : store.serialCategories(); + }), + getSelectedCategory: computed(() => { + const categoryId = store.selectedCategoryId(); + return [ + ...store.vodCategories(), + ...store.liveCategories(), + ...store.serialCategories(), + ].find((c) => c.id === categoryId); + }), + getImportCount: computed(() => store.importCount()), + getSelectedItemById: computed(() => { + const categoryType = store.selectedContentType(); + const content = + categoryType === 'live' + ? store.liveStreams() + : categoryType === 'vod' + ? store.vodStreams() + : store.serialStreams(); - if (!store.selectedItem()) return null; + if (!store.selectedItem()) return null; - return content.find( - (item) => - (item as any).stream_id === - store.selectedItem().stream_id || - (item as any).id === store.selectedItem().id - ); - }), - getPaginatedContent: computed(() => { - const startIndex = (store.page() - 1) * store.limit(); - const endIndex = startIndex + store.limit(); - const categoryId = store.selectedCategoryId(); + return content.find( + (item) => + (item as any).stream_id === + store.selectedItem().stream_id || + (item as any).id === store.selectedItem().id + ); + }), + getPaginatedContent: computed(() => { + const startIndex = (store.page() - 1) * store.limit(); + const endIndex = startIndex + store.limit(); + const categoryId = store.selectedCategoryId(); - if (!categoryId) { - return []; - } + if (!categoryId) { + return []; + } - const categoryType = store.selectedContentType(); - const content = - categoryType === 'live' - ? store.liveStreams() - : categoryType === 'vod' - ? store.vodStreams() - : store.serialStreams(); + const categoryType = store.selectedContentType(); + const content = + categoryType === 'live' + ? store.liveStreams() + : categoryType === 'vod' + ? store.vodStreams() + : store.serialStreams(); - return content - .filter((item) => item.category_id === categoryId) - .slice(startIndex, endIndex); - }), - getTotalPages: computed(() => { - const categoryId = store.selectedCategoryId(); - if (!categoryId) return 0; + return content + .filter((item) => item.category_id === categoryId) + .slice(startIndex, endIndex); + }), + getTotalPages: computed(() => { + const categoryId = store.selectedCategoryId(); + if (!categoryId) return 0; - const categoryType = store.selectedContentType(); - const totalItems = - categoryType === 'live' - ? store - .liveStreams() - .filter( - (i) => Number((i as any).id) === categoryId - ).length - : categoryType === 'vod' - ? store - .vodStreams() - .filter( - (i) => Number(i.category_id) === categoryId - ).length - : store - .serialStreams() - .filter((i) => i.category_id === categoryId) - .length; + const categoryType = store.selectedContentType(); + const totalItems = + categoryType === 'live' + ? store + .liveStreams() + .filter((i) => Number((i as any).id) === categoryId) + .length + : categoryType === 'vod' + ? store + .vodStreams() + .filter((i) => Number(i.category_id) === categoryId) + .length + : store + .serialStreams() + .filter((i) => i.category_id === categoryId).length; - return Math.ceil(totalItems / store.limit()); - }), - selectItemsFromSelectedCategory: computed(() => { - const categoryId = store.selectedCategoryId(); - const categoryType = store.selectedContentType(); - const content = - categoryType === 'live' - ? store.liveStreams() - : categoryType === 'vod' - ? store.vodStreams() - : store.serialStreams(); + return Math.ceil(totalItems / store.limit()); + }), + selectItemsFromSelectedCategory: computed(() => { + const categoryId = store.selectedCategoryId(); + const categoryType = store.selectedContentType(); + const content = + categoryType === 'live' + ? store.liveStreams() + : categoryType === 'vod' + ? store.vodStreams() + : store.serialStreams(); - return content.filter( - (item) => item.category_id === categoryId - ); - }), - }; - }), + return content.filter((item) => item.category_id === categoryId); + }), + globalRecentItems: computed(() => { + return store.recentItems(); // Remove the filter since we're getting global items directly from DB + }), + })), withMethods( ( store, @@ -329,14 +327,18 @@ export const XtreamStore = signalStore( const searchContent = async ( searchTerm: string, - types: string[] + types: string[], + route = inject(ActivatedRoute), + dbService = inject(DatabaseService) ) => { + if (!route.snapshot.params.id) return; + const results = await dbService.searchXtreamContent( route.snapshot.params.id, searchTerm, types ); - patchState(store, { searchResults: results }); + return results; }; const fetchXtreamPlaylist = rxMethod( @@ -467,7 +469,13 @@ export const XtreamStore = signalStore( }>( pipe( switchMap(({ term, types }) => - from(searchContent(term, types)) + from(searchContent(term, types, route, dbService)).pipe( + tap((results) => { + patchState(store, { + searchResults: results || [], + }); + }) + ) ) ) ); @@ -694,7 +702,10 @@ export const XtreamStore = signalStore( }); }, resetSearchResults() { - patchState(store, { searchResults: [] }); + patchState(store, { + searchResults: [], + globalSearchResults: [], + }); }, async loadEpg() { console.log('Loading EPG...'); @@ -773,6 +784,12 @@ export const XtreamStore = signalStore( patchState(store, { hideExternalInfoDialog }); }, checkPortalStatus, + setGlobalSearchResults(results: any[]) { + patchState(store, { + searchResults: results, + globalSearchResults: results, + }); + }, }; } )