diff --git a/src/app/stalker/stalker-main-container.component.html b/src/app/stalker/stalker-main-container.component.html new file mode 100644 index 000000000..62530d445 --- /dev/null +++ b/src/app/stalker/stalker-main-container.component.html @@ -0,0 +1,48 @@ + + + + + + + + + + + diff --git a/src/app/stalker/stalker-main-container.component.ts b/src/app/stalker/stalker-main-container.component.ts new file mode 100644 index 000000000..b3c4dca05 --- /dev/null +++ b/src/app/stalker/stalker-main-container.component.ts @@ -0,0 +1,443 @@ +import { AsyncPipe, NgIf } from '@angular/common'; +import { Component, NgZone, OnInit, Signal, effect } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { MatDialog } from '@angular/material/dialog'; +import { MatPaginatorModule, PageEvent } from '@angular/material/paginator'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { Store } from '@ngrx/store'; +import { StorageMap } from '@ngx-pwa/local-storage'; +import { Observable } from 'rxjs'; +import { IpcCommand } from '../../../shared/ipc-command.class'; +import { + ERROR, + OPEN_MPV_PLAYER, + OPEN_VLC_PLAYER, + STALKER_REQUEST, + STALKER_RESPONSE, +} from '../../../shared/ipc-commands'; +import { StalkerPortalActions } from '../../../shared/stalker-portal-actions.enum'; +import { DataService } from '../services/data.service'; +import { PlaylistsService } from '../services/playlists.service'; +import { Settings, VideoPlayer } from '../settings/settings.interface'; +import { STORE_KEY } from '../shared/enums/store-keys.enum'; +import { selectCurrentPlaylist } from '../state/selectors'; +import { Breadcrumb } from '../xtream/breadcrumb.interface'; +import { CategoryContentViewComponent } from '../xtream/category-content-view/category-content-view.component'; +import { CategoryViewComponent } from '../xtream/category-view/category-view.component'; +import { ContentTypeNavigationItem } from '../xtream/content-type-navigation-item.interface'; +import { ContentType } from '../xtream/content-type.enum'; +import { NavigationBarComponent } from '../xtream/navigation-bar/navigation-bar.component'; +import { PlayerDialogComponent } from '../xtream/player-dialog/player-dialog.component'; +import { PortalStore } from '../xtream/portal.store'; +import { VodDetailsComponent } from '../xtream/vod-details/vod-details.component'; +import { StalkerContentTypes } from './stalker-content-types'; + +@Component({ + selector: 'app-stalker-main-container', + templateUrl: './stalker-main-container.component.html', + /* styles: [ + ` + :host { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + } + `, + ], */ + standalone: true, + imports: [ + AsyncPipe, + CategoryViewComponent, + CategoryContentViewComponent, + NavigationBarComponent, + NgIf, + MatPaginatorModule, + VodDetailsComponent, + ], +}) +export class StalkerMainContainerComponent implements OnInit { + breadcrumbs: Breadcrumb[] = []; + currentPlaylist = this.store.selectSignal(selectCurrentPlaylist); + listeners = []; + isLoading = true; + selectedContentType: ContentType = ContentType.VODS; + currentLayout = 'category'; + searchPhrase = this.portalStore.searchPhrase(); + settings = toSignal( + this.storage.get(STORE_KEY.Settings) + ) as Signal; + favorites$: Observable; + + items: any[] = []; + itemDetails!: { + id: string; + cmd: string; + info: { + movie_image: string; + description: string; + name: string; + actors: string; + director: string; + releasedate: string; + genre: string; + }; + }; + + navigationContentTypes: ContentTypeNavigationItem[] = [ + { + contentType: ContentType.ITV, + label: 'Live Streams', + }, + { + contentType: ContentType.VODS, + label: 'VOD Streams', + }, + { + contentType: ContentType.FAVORITES, + label: 'Favorites', + }, + ]; + + currentCategoryId; + + //pagination + length = 0; + pageSize = 14; + pageIndex = 0; + + commandsList = [ + new IpcCommand(STALKER_RESPONSE, (response: any) => + this.handleResponse(response) + ), + new IpcCommand(ERROR, (response: { message: string; status: number }) => + this.showErrorAsNotification(response) + ), + ]; + + constructor( + private dataService: DataService, + private dialog: MatDialog, + private ngZone: NgZone, + private playlistService: PlaylistsService, + private portalStore: PortalStore, + private snackBar: MatSnackBar, + private storage: StorageMap, + private store: Store + ) { + effect(() => { + if (this.currentPlaylist()) { + if ( + this.currentPlaylist().password && + this.currentPlaylist().username + ) { + this.handshake(); + } else { + this.selectedContentType = ContentType.VODS; + this.getCategories(this.selectedContentType); + } + + this.favorites$ = this.playlistService.getPortalFavorites( + this.currentPlaylist()._id + ); + } + }); + + this.portalStore.setSearchPhrase(''); + } + + ngOnInit() { + this.commandsList.forEach((command) => { + if (this.dataService.isElectron) { + this.dataService.listenOn(command.id, (_event, response) => + this.ngZone.run(() => command.callback(response)) + ); + } else { + const cb = (response) => { + if (response.data.type === command.id) { + command.callback(response.data); + } + }; + this.dataService.listenOn(command.id, cb); + this.listeners.push(cb); + } + }); + } + + handshake() { + this.sendRequest({ action: 'handshake', type: 'stb' }); + } + + setInitialBreadcrumb( + action = StalkerPortalActions.GetCategories, + title = 'All categories' + ) { + this.breadcrumbs = [{ title, action }]; + } + + getCategories(contentType: ContentType = this.selectedContentType) { + this.selectedContentType = contentType; + this.currentLayout = 'category'; + const action = StalkerContentTypes[contentType].getCategoryAction; + this.setInitialBreadcrumb(action); + this.sendRequest({ action, type: contentType }); + } + + sendRequest(params: Record) { + this.isLoading = true; + if (params.action !== 'create_link') this.items = []; + const { portalUrl, macAddress } = this.currentPlaylist(); + let token = {}; + if (sessionStorage.getItem(this.currentPlaylist()._id)) { + token = sessionStorage.getItem(this.currentPlaylist()._id); + } + this.dataService.sendIpcEvent(STALKER_REQUEST, { + url: portalUrl, + macAddress, + params: { + ...params, + token, + }, + }); + } + + showErrorAsNotification(response: { message: string; status: number }) { + this.snackBar.open( + `Error: ${response?.message ?? 'Something went wrong'} (Status: ${ + response?.status ?? 0 + })`, + null, + { duration: 4000 } + ); + this.isLoading = false; + } + + handleResponse(response: { + action: string; + payload: { js: any; cmd?: string }; + }) { + if ( + response.action === StalkerPortalActions.GetCategories || + response.action === StalkerPortalActions.GetGenres + ) { + this.items = response.payload.js.map((item) => ({ + category_name: item.title, + category_id: item.id, + })); + } else if (response.action === StalkerPortalActions.GetOrderedList) { + if (response.payload.js.data && response.payload.js.data.length > 0) + this.portalStore.setContent(response.payload.js.data); + + this.items = response.payload.js.data.map((item) => ({ + ...item, + cover: item.screenshot_uri, + })); + this.length = response.payload.js.total_items; + this.pageIndex = response.payload.js.cur_page; + } else if (response.action === StalkerPortalActions.CreateLink) { + let url = response.payload.js.cmd as string; + if (url?.startsWith('ffmpeg')) { + url = url.split(' ')[1]; + } + + this.openPlayer(url, response.payload.js.name); + } else if (response.action === 'handshake') { + const token = response.payload.js.token; + sessionStorage.setItem(this.currentPlaylist()._id, token); + + this.sendRequest({ + action: 'do_auth', + login: this.currentPlaylist().username, + password: this.currentPlaylist().password, + type: 'stb', + token, + }); + } else if (response.action === 'do_auth') { + this.getCategories(); + } + } + + openPlayer(streamUrl: string, title: string) { + const player = this.settings().player; + if (player === VideoPlayer.MPV) { + this.dataService.sendIpcEvent(OPEN_MPV_PLAYER, { + url: streamUrl, + }); + } else if (player === VideoPlayer.VLC) { + this.dataService.sendIpcEvent(OPEN_VLC_PLAYER, { + url: streamUrl, + }); + } else { + this.dialog.open(PlayerDialogComponent, { + data: { streamUrl, player, title }, + width: '80%', + }); + } + } + + categoryClicked(item: { category_name: string; category_id: string }) { + this.currentLayout = 'category_content'; + this.currentCategoryId = item.category_id; + const action = + StalkerContentTypes[this.selectedContentType].getContentAction; + this.portalStore.setSearchPhrase(''); + + this.breadcrumbs.push({ + title: item.category_name, + category_id: item.category_id, + action, + }); + + this.sendRequest({ + action, + type: this.selectedContentType, + category: item.category_id, + genre: item.category_id, + }); + } + + itemClicked(i: any) { + if (this.selectedContentType === ContentType.ITV) { + this.playVod(i.cmd); + return; + } + const selectedContent = this.portalStore.getContentById(i.id)(); + this.itemDetails = !selectedContent + ? i.details // to read from favorites + : { + id: i.id, + cmd: selectedContent.cmd, + info: { + movie_image: selectedContent.screenshot_uri, + description: selectedContent.description, + name: selectedContent.name, + director: selectedContent.director, + releasedate: selectedContent.year, + genre: selectedContent.genres_str, + actors: selectedContent.actors, + }, + }; + this.breadcrumbs.push({ + title: this.itemDetails?.info?.name, + action: StalkerPortalActions.GetOrderedList, + }); + this.currentLayout = 'vod-details'; + } + + playVod(cmd?: string) { + const command = cmd ?? this.itemDetails.cmd; + const action = StalkerContentTypes[this.selectedContentType].getLink; + this.sendRequest({ + action, + type: this.selectedContentType, + cmd: command, + forced_storage: 'undefined', + disable_ad: '0', + JsHttpRequest: '1-xml', + }); + } + + breadcrumbClicked(breadcrumb: Breadcrumb) { + const itemIndex = this.breadcrumbs.findIndex((i) => i === breadcrumb); + + // do nothing if last breadcrumb child was clicked + if (itemIndex === this.breadcrumbs.length - 1) return; + + this.breadcrumbs.splice( + itemIndex + 1, + this.breadcrumbs.length - itemIndex - 1 + ); + + if (breadcrumb.action === StalkerPortalActions.GetOrderedList) { + this.currentLayout = 'category_content'; + } else if ( + breadcrumb.action === StalkerPortalActions.GetCategories || + breadcrumb.action === StalkerPortalActions.GetGenres + ) { + this.currentLayout = 'category'; + } else if (breadcrumb.action === StalkerPortalActions.Favorites) { + this.currentLayout = 'favorites'; + } + + this.currentCategoryId = breadcrumb.category_id; + this.sendRequest({ + action: breadcrumb.action, + type: this.selectedContentType, + ...(breadcrumb.category_id + ? { + category: breadcrumb.category_id, + genre: breadcrumb.category_id, + } + : {}), + }); + } + + addToFavorites(item: any) { + this.playlistService + .addPortalFavorite(this.currentPlaylist()._id, item) + .subscribe(() => { + this.snackBar.open('Added to favorites', null, { + duration: 1000, + }); + }); + } + + removeFromFavorites(favoriteId: string) { + this.playlistService + .removeFromPortalFavorites(this.currentPlaylist()._id, favoriteId) + .subscribe(() => { + this.snackBar.open('Removed from favorites', null, { + duration: 1000, + }); + }); + } + + setSearchPhrase(searchPhrase: string) { + if ( + this.currentLayout === 'category_content' && + this.searchPhrase !== searchPhrase + ) + this.sendRequest({ + action: StalkerContentTypes[this.selectedContentType] + .getContentAction, + type: this.selectedContentType, + search: searchPhrase, + ...(this.currentCategoryId + ? { + category: this.currentCategoryId, + genre: this.currentCategoryId, + } + : {}), + }); + } + + handlePageChange(event: PageEvent) { + this.sendRequest({ + action: StalkerContentTypes[this.selectedContentType] + .getContentAction, + type: this.selectedContentType, + category: this.currentCategoryId, + p: event.pageIndex + 1, + }); + } + + favoritesClicked() { + this.currentLayout = 'favorites'; + this.setInitialBreadcrumb( + StalkerPortalActions.Favorites, + 'My favorites' + ); + } + + ngOnDestroy(): void { + if (this.dataService.isElectron) { + this.commandsList.forEach((command) => + this.dataService.removeAllListeners(command.id) + ); + } else { + this.listeners.forEach((listener) => { + window.removeEventListener('message', listener); + }); + } + } +}