diff --git a/apps/web/src/app/xtream-tauri/stores/xtream.store.ts b/apps/web/src/app/xtream-tauri/stores/xtream.store.ts new file mode 100644 index 000000000..1f2970700 --- /dev/null +++ b/apps/web/src/app/xtream-tauri/stores/xtream.store.ts @@ -0,0 +1,225 @@ +import { computed, inject } from '@angular/core'; +import { + signalStore, + withComputed, + withMethods, +} from '@ngrx/signals'; +import { Store } from '@ngrx/store'; +import { selectActivePlaylist } from 'm3u-state'; +import { DataService } from 'services'; +import { + XtreamCodeActions, + XtreamSerieDetails, + XtreamVodDetails, +} from 'shared-interfaces'; + +// Import existing features that are already separate +import { withFavorites } from '../with-favorites.feature'; +import { withRecentItems } from '../with-recent-items'; + +// Import new feature stores +import { + withPortal, + withContent, + withSelection, + withSearch, + withEpg, + withPlayer, +} from './features'; + +/** + * XtreamStore - Facade composing all feature stores. + * + * This store provides a unified API for components while delegating + * to specialized feature stores for different concerns: + * + * - withPortal: Playlist and portal status management + * - withContent: Categories and streams management + * - withSelection: UI selection and pagination + * - withSearch: Search functionality + * - withEpg: EPG (Electronic Program Guide) data + * - withPlayer: Stream URL construction and player integration + * - withFavorites: Favorites management + * - withRecentItems: Recently viewed items + * + * @see docs/XTREAM_STORE_REFACTORING_PLAN.md + */ +export const XtreamStore = signalStore( + // Note: NOT providedIn: 'root' because shell provides it + // This matches the original behavior + + // Compose all features + withPortal(), + withContent(), + withSelection(), + withSearch(), + withEpg(), + withPlayer(), + withFavorites(), + withRecentItems(), + + // Cross-feature computed properties + withComputed((store) => ({ + /** + * Get global recent items (from withRecentItems) + */ + globalRecentItems: computed(() => { + return store.recentItems(); + }), + + /** + * Alias for importCount for backward compatibility + */ + getImportCount: computed(() => store.importCount()), + })), + + // Cross-feature methods & orchestration + withMethods((store) => { + const dataService = inject(DataService); + const ngrxStore = inject(Store); + + return { + /** + * Full store reset for switching between playlists + */ + resetStore(newPlaylistId?: string): void { + store.resetPortal(); + store.resetContent(); + store.resetSelection(); + store.resetSearchResults(); + store.clearEpg(); + store.resetPlayer(); + + if (newPlaylistId) { + store.setPlaylistId(newPlaylistId); + } + }, + + /** + * Initialize the store for a playlist + */ + async initialize(): Promise { + await store.fetchPlaylist(); + await store.checkPortalStatus(); + await store.initializeContent(); + }, + + /** + * Fetch Xtream playlist (convenience alias) + */ + async fetchXtreamPlaylist(): Promise { + await store.fetchPlaylist(); + }, + + /** + * Fetch VOD details with metadata + * Accepts object format for backward compatibility with rxMethod callers + */ + fetchVodDetailsWithMetadata( + params: { vodId: string; categoryId: number } + ): void { + const playlist = ngrxStore.selectSignal(selectActivePlaylist)(); + if (!playlist) return; + + dataService.fetchData( + `${playlist.serverUrl}/player_api.php`, + { + action: XtreamCodeActions.GetVodInfo, + username: playlist.username, + password: playlist.password, + vod_id: params.vodId, + } + ).then((vodDetails: XtreamVodDetails) => { + store.setSelectedCategory(params.categoryId); + store.setSelectedItem({ + ...vodDetails, + stream_id: params.vodId, + }); + }).catch((error: unknown) => { + console.error('Error fetching VOD details:', error); + }); + }, + + /** + * Fetch series details with metadata + * Accepts object format for backward compatibility with rxMethod callers + */ + fetchSerialDetailsWithMetadata( + params: { serialId: string; categoryId: number } + ): void { + const playlist = ngrxStore.selectSignal(selectActivePlaylist)(); + if (!playlist) return; + + dataService.fetchData( + `${playlist.serverUrl}/player_api.php`, + { + action: XtreamCodeActions.GetSeriesInfo, + username: playlist.username, + password: playlist.password, + series_id: params.serialId, + } + ).then((serialDetails: XtreamSerieDetails) => { + store.setSelectedCategory(params.categoryId); + store.setSelectedItem({ + ...serialDetails, + series_id: params.serialId, + }); + }).catch((error: unknown) => { + console.error('Error fetching series details:', error); + }); + }, + + /** + * Legacy method stubs for backward compatibility + */ + createLinkToPlayVod(): void { + // No-op, kept for compatibility + }, + + addToFavorites(item: unknown): void { + console.log('Legacy addToFavorites called', item); + }, + + removeFromFavorites(favoriteId: string): void { + console.log('Legacy removeFromFavorites called', favoriteId); + }, + + // Alias methods for backward compatibility + fetchLiveCategories(): void { + store.fetchAllCategories(); + }, + + fetchVodCategories(): void { + store.fetchAllCategories(); + }, + + fetchSerialCategories(): void { + store.fetchAllCategories(); + }, + + fetchLiveStreams(): void { + store.fetchAllContent(); + }, + + fetchVodStreams(): void { + store.fetchAllContent(); + }, + + fetchSerialStreams(): void { + store.fetchAllContent(); + }, + + /** + * Search content wrapper for rxMethod compatibility + * Can be called with object { term, types } or direct params + */ + searchContent(params: { term: string; types: string[] }): void { + // Call the underlying search method from withSearch + (store as any).searchContent(params.term, params.types); + }, + }; + }) +); + +// Type alias for the store +export type XtreamStoreType = InstanceType; diff --git a/apps/web/src/app/xtream-tauri/xtream.store.ts b/apps/web/src/app/xtream-tauri/xtream.store.ts deleted file mode 100644 index 327eaf98c..000000000 --- a/apps/web/src/app/xtream-tauri/xtream.store.ts +++ /dev/null @@ -1,968 +0,0 @@ -import { computed, inject } from '@angular/core'; -import { - patchState, - signalStore, - withComputed, - withMethods, - withState, -} from '@ngrx/signals'; -import { rxMethod } from '@ngrx/signals/rxjs-interop'; -import { Store } from '@ngrx/store'; -import { selectActivePlaylist } from 'm3u-state'; -import { - catchError, - combineLatestWith, - EMPTY, - from, - lastValueFrom, - pipe, - switchMap, - tap, -} from 'rxjs'; -import { DatabaseService, DataService } from 'services'; -import { - Playlist, - XtreamCodeActions, - XtreamSerieDetails, - XtreamSerieEpisode, - XtreamVodDetails, -} from 'shared-interfaces'; -import { PlayerService } from '../services/player.service'; -import { SettingsStore } from '../services/settings-store.service'; -import { XtreamAccountInfo } from './account-info/account-info.interface'; -import { withFavorites } from './with-favorites.feature'; -import { withRecentItems } from './with-recent-items'; -import { XtreamState } from './xtream-state'; - -const initialState: XtreamState = { - isLoadingCategories: false, - isLoadingContent: false, - isImporting: false, - itemsToImport: 0, - liveCategories: [], - vodCategories: [], - serialCategories: [], - liveStreams: [], - vodStreams: [], - serialStreams: [], - page: 0, - limit: Number(localStorage.getItem('xtream-page-size') ?? 25), - selectedCategoryId: null, - searchResults: [], - selectedContentType: 'vod', - selectedItem: null, - importCount: 0, - currentPlaylist: null, - epgItems: [], - hideExternalInfoDialog: - localStorage.getItem('hideExternalInfoDialog') === 'true', - portalStatus: 'unavailable', - globalSearchResults: [], - streamUrl: null, - playlistId: null, -}; - -/** to decode epg */ -function b64DecodeUnicode(str: string) { - return decodeURIComponent( - Array.prototype.map - .call(atob(str), function (c) { - return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); - }) - .join('') - ); -} - -export const XtreamStore = signalStore( - { providedIn: 'root' }, - withState(initialState), - withRecentItems(), - withFavorites(), - withComputed((store) => ({ - getCategoriesBySelectedType: computed(() => { - const type = store.selectedContentType(); - return type === 'live' - ? store.liveCategories() - : type === 'vod' - ? store.vodCategories() - : store.serialCategories(); - }), - /** - * Memoized category item counts map. - * Returns a Map for the currently selected content type. - * The Map is recomputed only when streams or content type changes. - */ - getCategoryItemCounts: computed(() => { - const type = store.selectedContentType(); - const streams = - type === 'live' - ? store.liveStreams() - : type === 'vod' - ? store.vodStreams() - : store.serialStreams(); - - const countMap = new Map(); - for (const item of streams) { - const catId = Number(item.category_id); - if (!isNaN(catId)) { - countMap.set(catId, (countMap.get(catId) || 0) + 1); - } - } - return countMap; - }), - 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; - - 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() * store.limit(); - const endIndex = startIndex + store.limit(); - const categoryId = store.selectedCategoryId(); - - if (!categoryId) { - return []; - } - - 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); - }), - isPaginatedContentLoading: computed(() => store.isLoadingContent()), - 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; - - 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); - }), - globalRecentItems: computed(() => { - return store.recentItems(); // Remove the filter since we're getting global items directly from DB - }), - })), - withMethods( - ( - store, - dataService = inject(DataService), - dbService = inject(DatabaseService), - oldStore = inject(Store), - settingsStore = inject(SettingsStore), - playerService = inject(PlayerService) - ) => { - const fetchCategories = ( - action: XtreamCodeActions, - stateKey: keyof Pick< - XtreamState, - 'liveCategories' | 'vodCategories' | 'serialCategories' - >, - type: 'live' | 'movies' | 'series' - ) => { - const playlistId = store.playlistId(); - console.log(`Fetching ${type} categories for playlist ${playlistId}...`); - patchState(store, { isLoadingCategories: true }); - const queryParams = { - action, - username: store.currentPlaylist().username, - password: store.currentPlaylist().password, - }; - - return from( - dbService.hasXtreamCategories(playlistId, type) - ).pipe( - switchMap(async (exists) => { - try { - if (exists) { - const localData = - await dbService.getXtreamCategories( - playlistId, - type - ); - patchState(store, { - [stateKey]: localData, - isLoadingCategories: false, - }); - return localData; - } - - const remoteData = await dataService.fetchData( - `${ - store.currentPlaylist().serverUrl - }/player_api.php`, - queryParams - ); - - if (remoteData && Array.isArray(remoteData)) { - await dbService.saveXtreamCategories( - playlistId, - remoteData, - type - ); - } - - const localData = - await dbService.getXtreamCategories( - playlistId, - type - ); - patchState(store, { - [stateKey]: localData, - isLoadingCategories: false, - }); - - return localData; - } catch (err) { - console.error( - `Error in fetchCategories for ${type}:`, - err - ); - patchState(store, { isLoadingCategories: false }); - throw err; - } - }) - ); - }; - - const fetchStreams = ( - action: XtreamCodeActions, - stateKey: keyof Pick< - XtreamState, - 'liveStreams' | 'vodStreams' | 'serialStreams' - >, - type: 'live' | 'movie' | 'series' - ) => { - const playlistId = store.playlistId(); - console.log(`Fetching ${type} streams for playlist ${playlistId}...`); - patchState(store, { isLoadingContent: true }); - const queryParams = { - action, - username: store.currentPlaylist().username, - password: store.currentPlaylist().password, - }; - - return from( - dbService.hasXtreamContent(playlistId, type) - ).pipe( - switchMap(async (exists) => { - try { - if (exists) { - const localData = - await dbService.getXtreamContent( - playlistId, - type - ); - patchState(store, { - [stateKey]: localData, - isLoadingContent: false, - }); - return localData; - } - - const remoteData = await dataService.fetchData( - `${ - store.currentPlaylist().serverUrl - }/player_api.php`, - queryParams - ); - - if (remoteData && Array.isArray(remoteData)) { - patchState(store, { - itemsToImport: - store.itemsToImport() + - remoteData.length, - }); - const insertedCount = - await dbService.saveXtreamContent( - playlistId, - remoteData, - type, - (count) => - patchState(store, { - importCount: count, - }) - ); - console.log(`Inserted ${insertedCount} items`); - } - - const localData = await dbService.getXtreamContent( - playlistId, - type - ); - patchState(store, { - [stateKey]: localData, - isLoadingContent: false, - }); - - return localData; - } catch (err) { - console.error('Error in fetchStreams:', err); - patchState(store, { isLoadingContent: false }); - throw err; - } - }) - ); - }; - - const searchContent = async ( - searchTerm: string, - types: string[] - ) => { - const playlistId = store.playlistId(); - if (!playlistId) return; - - const results = await dbService.searchXtreamContent( - playlistId, - searchTerm, - types - ); - return results; - }; - - const fetchXtreamPlaylist = async () => { - const playlistId = store.playlistId(); - console.log('fetchXtreamPlaylist called with playlistId:', playlistId); - if (!playlistId) { - console.log('No playlistId, skipping fetch'); - return; - } - - try { - const playlist = await dbService.getPlaylistById(playlistId); - console.log('Fetched playlist from DB:', playlist); - - if (playlist) { - patchState(store, { - currentPlaylist: playlist, - }); - } else { - const activePlaylist = oldStore.selectSignal( - selectActivePlaylist - )() as Playlist; - console.log('No playlist in DB, using active playlist:', activePlaylist); - - if (activePlaylist) { - await dbService.createPlaylist(activePlaylist); - patchState(store, { - currentPlaylist: { - ...activePlaylist, - serverUrl: activePlaylist.serverUrl, - id: activePlaylist._id, - name: activePlaylist.title, - }, - }); - } - } - } catch (error) { - console.error('Error fetching playlist:', error); - } - }; - - const fetchLiveCategories = rxMethod( - pipe( - switchMap(() => - fetchCategories( - XtreamCodeActions.GetLiveCategories, - 'liveCategories', - 'live' - ) - ) - ) - ); - - const fetchVodCategories = rxMethod( - pipe( - switchMap(() => - fetchCategories( - XtreamCodeActions.GetVodCategories, - 'vodCategories', - 'movies' - ) - ) - ) - ); - - const fetchSerialCategories = rxMethod( - pipe( - switchMap(() => - fetchCategories( - XtreamCodeActions.GetSeriesCategories, - 'serialCategories', - 'series' - ) - ) - ) - ); - - const fetchLiveStreams = rxMethod( - pipe( - switchMap(() => - fetchStreams( - XtreamCodeActions.GetLiveStreams, - 'liveStreams', - 'live' - ) - ) - ) - ); - - const fetchVodStreams = rxMethod( - pipe( - switchMap(() => - fetchStreams( - XtreamCodeActions.GetVodStreams, - 'vodStreams', - 'movie' - ) - ) - ) - ); - - const fetchSerialStreams = rxMethod( - pipe( - switchMap(() => - fetchStreams( - XtreamCodeActions.GetSeries, - 'serialStreams', - 'series' - ) - ) - ) - ); - - const setPage = (page: number) => patchState(store, { page }); - const setLimit = (limit: number) => patchState(store, { limit }); - const setSelectedCategory = (categoryId: number) => { - patchState(store, { - selectedCategoryId: Number(categoryId), - page: 0, - }); - }; - const setSelectedContentType = (type: 'live' | 'vod' | 'series') => - patchState(store, { - selectedContentType: type, - selectedCategoryId: null, - }); - const setSelectedItem = (item: any) => - patchState(store, { selectedItem: item }); - const searchContentMethod = rxMethod<{ - term: string; - types: string[]; - }>( - pipe( - switchMap(({ term, types }) => - from(searchContent(term, types)).pipe( - tap((results) => { - patchState(store, { - searchResults: results || [], - }); - }) - ) - ) - ) - ); - const fetchVodDetailsWithMetadata = rxMethod<{ - vodId: string; - categoryId: number; - }>( - pipe( - combineLatestWith(oldStore.select(selectActivePlaylist)), - switchMap(([{ vodId, categoryId }, playlist]) => { - if (!playlist) return EMPTY; - - return from( - dataService.fetchData( - `${playlist.serverUrl}/player_api.php`, - { - action: XtreamCodeActions.GetVodInfo, - username: playlist.username, - password: playlist.password, - vod_id: vodId, - } - ) - ).pipe( - tap((currentVod: XtreamVodDetails) => { - patchState(store, { - selectedCategoryId: Number(categoryId), - }); - patchState(store, { - selectedItem: { - ...currentVod, - stream_id: vodId, - }, - }); - }) - ); - }) - ) - ); - - const fetchSerialDetailsWithMetadata = rxMethod<{ - serialId: string; - categoryId: number; - }>( - pipe( - combineLatestWith(oldStore.select(selectActivePlaylist)), - switchMap(([{ serialId, categoryId }, playlist]) => { - if (!playlist) return EMPTY; - - return from( - dataService.fetchData( - `${playlist.serverUrl}/player_api.php`, - { - action: XtreamCodeActions.GetSeriesInfo, - username: playlist.username, - password: playlist.password, - series_id: serialId, - } - ) - ).pipe( - tap((currentSerial: XtreamSerieDetails) => { - patchState(store, { - selectedCategoryId: Number(categoryId), - }); - patchState(store, { - selectedItem: { - ...currentSerial, - series_id: serialId, - }, - }); - }) - ); - }) - ) - ); - - const initializeContent = async () => { - patchState(store, { isImporting: true }); - try { - console.log('Starting content initialization...'); - - // Fetch all categories in parallel - console.log('Fetching categories...'); - await Promise.all([ - lastValueFrom( - fetchCategories( - XtreamCodeActions.GetLiveCategories, - 'liveCategories', - 'live' - ) - ), - lastValueFrom( - fetchCategories( - XtreamCodeActions.GetVodCategories, - 'vodCategories', - 'movies' - ) - ), - lastValueFrom( - fetchCategories( - XtreamCodeActions.GetSeriesCategories, - 'serialCategories', - 'series' - ) - ), - ]); - - // Fetch all content in parallel - console.log('Fetching content...'); - await Promise.all([ - lastValueFrom( - fetchStreams( - XtreamCodeActions.GetLiveStreams, - 'liveStreams', - 'live' - ) - ), - lastValueFrom( - fetchStreams( - XtreamCodeActions.GetVodStreams, - 'vodStreams', - 'movie' - ) - ), - lastValueFrom( - fetchStreams( - XtreamCodeActions.GetSeries, - 'serialStreams', - 'series' - ) - ), - ]); - console.log('Content initialization completed'); - - // Check if we need to restore favorites and recently viewed after refresh - const currentPlaylistId = store.playlistId(); - if (currentPlaylistId) { - const restoreKey = `xtream-restore-${currentPlaylistId}`; - const restoreData = localStorage.getItem(restoreKey); - if (restoreData) { - try { - const { - favoritedXtreamIds, - recentlyViewedXtreamIds, - } = JSON.parse(restoreData); - await dbService.restoreXtreamUserData( - currentPlaylistId, - favoritedXtreamIds, - recentlyViewedXtreamIds - ); - localStorage.removeItem(restoreKey); - } catch (err) { - console.error( - 'Error restoring user data:', - err - ); - } - } - } - } catch (error) { - console.error( - 'Error during content initialization:', - error - ); - } finally { - patchState(store, { isImporting: false }); - } - }; - - const checkPortalStatus = rxMethod( - pipe( - combineLatestWith(oldStore.select(selectActivePlaylist)), - switchMap(([, playlist]) => { - if (!playlist) return EMPTY; - - return from( - dataService.fetchData( - `${playlist.serverUrl}/player_api.php`, - { - username: playlist.username, - password: playlist.password, - action: 'get_account_info', - } - ) - ).pipe( - tap((response: XtreamAccountInfo) => { - console.log( - 'Portal status response:', - response - ); - - if (!response?.user_info?.status) { - patchState(store, { - portalStatus: 'unavailable', - }); - return; - } - - if (response.user_info.status === 'Active') { - const expDate = new Date( - parseInt(response.user_info.exp_date) * - 1000 - ); - if (expDate < new Date()) { - patchState(store, { - portalStatus: 'expired', - }); - } else { - patchState(store, { - portalStatus: 'active', - }); - } - } else { - patchState(store, { - portalStatus: 'inactive', - }); - } - }), - catchError((error) => { - console.error( - 'Error checking portal status:', - error - ); - patchState(store, { - portalStatus: 'unavailable', - }); - return EMPTY; - }) - ); - }) - ) - ); - - return { - createLinkToPlayVod: () => {}, - fetchXtreamPlaylist, - fetchLiveCategories, - fetchVodCategories, - fetchSerialCategories, - fetchLiveStreams, - fetchVodStreams, - fetchSerialStreams, - setPage, - setLimit, - setSelectedCategory, - setSelectedContentType, - setSelectedItem, - searchContent: searchContentMethod, - fetchVodDetailsWithMetadata, - fetchSerialDetailsWithMetadata, - initializeContent, - /** - * Set the current playlist ID - */ - setPlaylistId(playlistId: string) { - patchState(store, { playlistId }); - }, - /** - * Reset store to initial state for switching between playlists - */ - resetStore(newPlaylistId?: string) { - patchState(store, { - ...initialState, - playlistId: newPlaylistId ?? null, - limit: Number( - localStorage.getItem('xtream-page-size') ?? 25 - ), - hideExternalInfoDialog: - localStorage.getItem('hideExternalInfoDialog') === - 'true', - }); - }, - updatePlaylist(playlist: any) { - patchState(store, { - currentPlaylist: { - ...store.currentPlaylist(), - playlist, - }, - }); - }, - resetSearchResults() { - patchState(store, { - searchResults: [], - globalSearchResults: [], - }); - }, - async loadEpg() { - console.log('Loading EPG...'); - const remoteData = await dataService.fetchData( - `${store.currentPlaylist().serverUrl}/player_api.php`, - { - action: 'get_short_epg', - username: store.currentPlaylist().username, - password: store.currentPlaylist().password, - stream_id: store.selectedItem().xtream_id, - limit: 10, - } - ); - - if ( - remoteData?.epg_listings && - Array.isArray(remoteData.epg_listings) - ) { - console.log(`Got epg data categories:`, remoteData); - patchState(store, { - epgItems: remoteData.epg_listings.map((i) => ({ - ...i, - title: b64DecodeUnicode(i.title).trim(), - description: b64DecodeUnicode( - i.description - ).trim(), - })), - }); - } else { - console.error( - 'Invalid remote data received:', - remoteData - ); - patchState(store, { - epgItems: [], - }); - return []; - } - }, - async loadChannelEpg(streamId: number) { - try { - const response = await dataService.fetchData( - `${ - store.currentPlaylist().serverUrl - }/player_api.php`, - { - action: 'get_short_epg', - username: store.currentPlaylist().username, - password: store.currentPlaylist().password, - stream_id: streamId, - limit: 1, - } - ); - - if ( - response?.epg_listings && - Array.isArray(response.epg_listings) - ) { - return response.epg_listings.map((item) => ({ - ...item, - title: b64DecodeUnicode(item.title).trim(), - description: b64DecodeUnicode( - item.description - ).trim(), - })); - } - return []; - } catch (error) { - console.error('Error loading channel EPG:', error); - return []; - } - }, - setHideExternalInfoDialog(hideExternalInfoDialog: boolean) { - localStorage.setItem( - 'hideExternalInfoDialog', - store.hideExternalInfoDialog.toString() - ); - patchState(store, { hideExternalInfoDialog }); - }, - checkPortalStatus, - setGlobalSearchResults(results: any[]) { - patchState(store, { - searchResults: results, - globalSearchResults: results, - }); - }, - constructStreamUrl(item: any) { - const { serverUrl, username, password } = - store.currentPlaylist(); - const streamFormat = settingsStore.streamFormat() ?? 'ts'; - const streamUrl = `${serverUrl}/live/${username}/${password}/${item.xtream_id}.${streamFormat}`; - - // Set the selected item and load EPG data in the store - setSelectedItem(item); - this.loadEpg(); - - patchState(store, { streamUrl }); - return streamUrl; - }, - constructVodStreamUrl(vodItem: XtreamVodDetails) { - const { serverUrl, username, password } = - store.currentPlaylist(); - const streamUrl = `${serverUrl}/movie/${username}/${password}/${vodItem.movie_data.stream_id}.${vodItem.movie_data.container_extension}`; - - patchState(store, { streamUrl }); - return streamUrl; - }, - constructEpisodeStreamUrl(episode: XtreamSerieEpisode) { - const { serverUrl, username, password } = - store.currentPlaylist(); - const streamUrl = `${serverUrl}/series/${username}/${password}/${episode.id}.${episode.container_extension}`; - patchState(store, { streamUrl }); - return streamUrl; - }, - openPlayer( - streamUrl: string, - title: string, - thumbnail: string = null - ) { - const playlist = store.currentPlaylist(); - playerService.openPlayer( - streamUrl, - title, - thumbnail, - localStorage.getItem('hideExternalInfoDialog') === - 'true', - store.selectedContentType() === 'live', - playlist?.userAgent, - playlist?.referrer, - playlist?.origin - ); - }, - addToFavorites(item: any) { - console.log('not needed now', item); - }, - removeFromFavorites(favoriteId: string) { - console.log('not needed now', favoriteId); - }, - /** - * Reload categories from database after visibility changes - */ - async reloadCategories() { - const playlistId = store.playlistId(); - if (!playlistId) return; - - try { - const [ - liveCategories, - vodCategories, - serialCategories, - ] = await Promise.all([ - dbService.getXtreamCategories(playlistId, 'live'), - dbService.getXtreamCategories(playlistId, 'movies'), - dbService.getXtreamCategories(playlistId, 'series'), - ]); - - patchState(store, { - liveCategories: liveCategories as any, - vodCategories: vodCategories as any, - serialCategories: serialCategories as any, - }); - } catch (error) { - console.error('Error reloading categories:', error); - } - }, - }; - } - ) -);