refactor: unify xtream feature stores

This commit is contained in:
4gray committed 2026-01-11 06:26:57 +01:00
1 parent 3a2ee74b96
commit 46a78f2d86
2 files changed
+225 -968

No files matched your search

@@ -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<void> {
await store.fetchPlaylist();
await store.checkPortalStatus();
await store.initializeContent();
},
/**
* Fetch Xtream playlist (convenience alias)
*/
async fetchXtreamPlaylist(): Promise<void> {
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<typeof XtreamStore>;
@@ -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<categoryId, count> 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<number, number>();
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<void>(
pipe(
switchMap(() =>
fetchCategories(
XtreamCodeActions.GetLiveCategories,
'liveCategories',
'live'
)
)
)
);
const fetchVodCategories = rxMethod<void>(
pipe(
switchMap(() =>
fetchCategories(
XtreamCodeActions.GetVodCategories,
'vodCategories',
'movies'
)
)
)
);
const fetchSerialCategories = rxMethod<void>(
pipe(
switchMap(() =>
fetchCategories(
XtreamCodeActions.GetSeriesCategories,
'serialCategories',
'series'
)
)
)
);
const fetchLiveStreams = rxMethod<void>(
pipe(
switchMap(() =>
fetchStreams(
XtreamCodeActions.GetLiveStreams,
'liveStreams',
'live'
)
)
)
);
const fetchVodStreams = rxMethod<void>(
pipe(
switchMap(() =>
fetchStreams(
XtreamCodeActions.GetVodStreams,
'vodStreams',
'movie'
)
)
)
);
const fetchSerialStreams = rxMethod<void>(
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<void>(
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);
}
},
};
}
)
);