mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 04:26:16 -08:00
refactor: unify xtream feature stores
This commit is contained in:
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);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in new issue
Block a user