feat: add xtream code api support

This commit is contained in:
4gray committed 2023-07-01 23:10:31 +02:00
1 parent d4a2741e45
commit eb87c0fe0a
34 files changed
+1451 -109

No files matched your search

@@ -0,0 +1,43 @@
<ng-container *ngIf="items.length > 0; else noItems">
<div class="search">
<input
class="search-input"
placeholder="Search"
[(ngModel)]="searchText"
type="search"
/>
</div>
<div class="grid">
<mat-card
*ngFor="
let item of items | filterBy : searchText : 'name';
trackBy: trackByFn
"
(click)="itemClicked.emit(item)"
>
<mat-card-content>
<img
class="stream-icon"
*ngIf="item.stream_icon"
[src]="item.stream_icon"
/>
<img
class="stream-icon"
*ngIf="item.cover"
[src]="item.cover"
/>
<div
class="stream-icon-placeholder"
*ngIf="!item.stream_icon && !item.cover"
></div>
<div class="title">{{ item.name }}</div>
</mat-card-content>
</mat-card>
</div>
</ng-container>
<ng-template #noItems>
<div class="no-content">
<mat-icon class="icon">list</mat-icon>
<div>This category is empty, try another one</div>
</div>
</ng-template>
@@ -0,0 +1,67 @@
:host {
display: block;
padding: 10px;
}
.search {
text-align: center;
margin-bottom: 10px;
.search-input {
padding: 10px;
width: 300px;
text-align: center;
border-radius: 5px;
border: 1px solid #333;
}
}
.grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
mat-card {
cursor: pointer;
width: 200px;
mat-card-content {
padding: 0;
margin: 0;
}
.stream-icon {
width: 100%;
display: block;
}
.stream-icon-placeholder {
width: 100%;
background: #ccc;
height: 300px;
&::after {
content: 'No cover';
position: absolute;
top: 130px;
left: 70px;
}
}
.title {
word-break: break-all;
padding: 8px;
}
}
}
.no-content {
text-align: center;
.icon {
font-size: 64px;
height: 64px;
width: 64px;
}
}
@@ -0,0 +1,32 @@
import { NgFor, NgIf } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { XtreamItem } from '../../../../shared/xtream-item.interface';
import { FilterPipe } from '../../shared/pipes/filter.pipe';
@Component({
selector: 'app-category-content-view',
templateUrl: './category-content-view.component.html',
styleUrls: ['./category-content-view.component.scss'],
standalone: true,
imports: [
NgFor,
MatCardModule,
MatIconModule,
NgIf,
FilterPipe,
FormsModule,
],
})
export class CategoryContentViewComponent {
@Input({ required: true }) items: XtreamItem[];
@Output() itemClicked = new EventEmitter<any>();
searchText: string;
trackByFn(_index: number, item: XtreamItem) {
return item.stream_id;
}
}
@@ -0,0 +1,110 @@
import { NgFor, NgIf } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { XtreamCategory } from '../../../../shared/xtream-category.interface';
import { FilterPipe } from '../../shared/pipes/filter.pipe';
@Component({
selector: 'app-category-view',
standalone: true,
template: `
<ng-container *ngIf="items?.length > 0; else noItems">
<div class="search">
<input
class="search-input"
placeholder="Search"
[(ngModel)]="searchText"
type="search"
/>
</div>
<div class="grid">
<mat-card
class="category-item"
*ngFor="
let item of items
| filterBy : searchText : 'category_name';
trackBy: trackByFn
"
(click)="categoryClicked.emit(item)"
>
<mat-card-content>
{{ item.category_name }}
</mat-card-content>
</mat-card>
</div>
</ng-container>
<ng-template #noItems>
<div class="no-content">
<mat-icon class="icon">warning</mat-icon>
<div>
Oops, no content here, please change the category or content
type
</div>
</div>
</ng-template>
`,
styles: [
`
:host {
margin: 10px;
display: block;
}
.no-content {
text-align: center;
.icon {
font-size: 64px;
height: 64px;
width: 64px;
}
}
.grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
.category-item {
cursor: pointer;
width: 200px;
}
}
.search {
text-align: center;
margin-bottom: 10px;
.search-input {
padding: 10px;
width: 300px;
text-align: center;
border-radius: 5px;
border: 1px solid #333;
}
}
`,
],
imports: [
FilterPipe,
FormsModule,
MatCardModule,
MatIconModule,
NgFor,
NgIf,
],
})
export class CategoryViewComponent {
@Input({ required: true }) items: XtreamCategory[];
@Output() categoryClicked = new EventEmitter<XtreamCategory>();
searchText: string;
trackByFn(_index: number, item: XtreamCategory) {
return item.category_id;
}
}
+5
View File
@@ -0,0 +1,5 @@
export enum ContentType {
LIVE_STREAMS = 'LIVE_STREAMS',
VODS = 'VODS',
SERIES = 'SERIES',
}
+53
View File
@@ -0,0 +1,53 @@
.container {
display: grid;
grid-gap: 10px;
grid-template-columns: 340px auto;
&::before {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-size: cover;
z-index: -1;
background-repeat: no-repeat;
}
}
.image {
width: 300px;
padding: 20px;
}
.image img {
max-width: 100%;
height: auto;
}
@media (max-width: 480px) {
.container {
display: flex;
flex-wrap: wrap;
margin: 0 auto;
max-width: 1200px;
}
.image,
.details {
flex: 1 1 100%;
}
}
.label {
font-weight: 100;
font-size: 0.9em;
padding: 5px 0;
text-decoration: underline;
opacity: 0.6;
}
.action-buttons {
margin-top: 10px;
}
@@ -0,0 +1,35 @@
<div id="navigation">
<button mat-icon-button routerLink="/">
<mat-icon>home</mat-icon>
</button>
<div>
<mat-button-toggle-group
name="contentType"
aria-label="Content Type"
[value]="contentType"
(change)="contentTypeChanged.emit($event.value)"
>
<mat-button-toggle [value]="ContentTypeEnum.LIVE_STREAMS"
>Live Streams</mat-button-toggle
>
<mat-button-toggle [value]="ContentTypeEnum.VODS"
>VOD Streams</mat-button-toggle
>
<mat-button-toggle [value]="ContentTypeEnum.SERIES"
>Series</mat-button-toggle
>
</mat-button-toggle-group>
</div>
<button mat-icon-button>
<mat-icon>info</mat-icon>
</button>
</div>
<div id="breadcrumbs">
<span
*ngFor="let item of breadcrumbs"
(click)="breadcrumbClicked.emit(item)"
>{{ item.title }}</span
>
</div>
@@ -0,0 +1,32 @@
:host {
position: sticky;
top: 0;
z-index: 1000;
}
#navigation {
height: 70px;
background: black;
display: flex;
justify-content: space-between;
align-items: center;
}
#breadcrumbs {
background: black;
border-bottom: 1px solid #333;
padding: 10px;
span {
cursor: pointer;
&:not(:last-child)::after {
content: '>';
margin: 0 10px;
}
}
span:hover {
text-decoration: underline;
}
}
@@ -0,0 +1,31 @@
import { NgFor } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatIconModule } from '@angular/material/icon';
import { RouterLink } from '@angular/router';
import { ContentType } from '../content-type.enum';
import { Breadcrumb } from '../xtream-main-container.component';
@Component({
selector: 'app-navigation-bar',
templateUrl: './navigation-bar.component.html',
styleUrls: ['./navigation-bar.component.scss'],
standalone: true,
imports: [
MatButtonToggleModule,
MatIconModule,
MatButtonModule,
RouterLink,
NgFor,
],
})
export class NavigationBarComponent {
@Input({ required: true }) breadcrumbs: Breadcrumb[];
@Input({ required: true }) contentType: ContentType;
@Output() contentTypeChanged = new EventEmitter<ContentType>();
@Output() breadcrumbClicked = new EventEmitter<Breadcrumb>();
ContentTypeEnum = ContentType;
}
@@ -0,0 +1,13 @@
<h2 mat-dialog-title>Title</h2>
<mat-dialog-content class="mat-typography">
<app-vjs-player
[options]="{
sources: [
{
src: streamUrl,
type: 'application/x-mpegURL'
}
]
}"
></app-vjs-player>
</mat-dialog-content>
@@ -0,0 +1,17 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { VjsPlayerComponent } from '../../player/components/vjs-player/vjs-player.component';
@Component({
selector: 'app-player-dialog',
templateUrl: './player-dialog.component.html',
standalone: true,
imports: [VjsPlayerComponent, MatDialogModule],
})
export class PlayerDialogComponent {
streamUrl: string;
constructor(@Inject(MAT_DIALOG_DATA) streamUrl: any) {
this.streamUrl = streamUrl;
}
}
@@ -0,0 +1,20 @@
<div class="centered">
<mat-icon class="icon">live_tv</mat-icon>
<div>
The video is playing in external player window.<br />
Please make sure that mpv player is correctly installed on your
system.<br />
See
<a
[routerLink]
style="cursor: pointer"
(click)="
openUrl(
'https://github.com/4gray/iptvnator/wiki/What-is-mpv-video-player-and-how-to-install-it-on-different-operating-systems%3F'
)
"
>installation instructions</a
>
for more details.
</div>
</div>
@@ -0,0 +1,30 @@
import { Component } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-player-view',
templateUrl: './player-view.component.html',
standalone: true,
imports: [MatIconModule, RouterLink],
styles: [
`
.centered {
text-align: center;
line-height: 24px;
margin: 20px;
.icon {
font-size: 64px;
height: 64px;
width: 64px;
}
}
`,
],
})
export class PlayerViewComponent {
openUrl(url: string) {
window.open(url, '_blank');
}
}
@@ -0,0 +1,30 @@
<h3>Seasons and episodes</h3>
<div class="seasons-container">
<ng-container *ngIf="!selectedSeason; else episodesView">
<mat-card
class="season-item"
*ngFor="let season of seasons | keyvalue"
(click)="selectSeason(season.key)"
>
<mat-card-content>Season {{ season.key }}</mat-card-content>
</mat-card>
</ng-container>
<ng-template #episodesView>
<mat-card class="episode-item" (click)="selectedSeason = undefined">
<mat-card-title>< Back to seasons</mat-card-title>
</mat-card>
<mat-card
class="episode-item"
*ngFor="let episode of seasons[selectedSeason]"
(click)="selectEpisode(episode)"
>
<mat-card-content
><img [src]="episode.info.movie_image" class="episode-cover"
/></mat-card-content>
<div class="episode-title">
{{ episode.episode_num }}. {{ episode.title }}
</div>
</mat-card>
</ng-template>
</div>
@@ -0,0 +1,36 @@
:host {
margin: 10px;
display: block;
padding: 20px 0;
}
.seasons-container {
width: 100%;
display: flex;
flex-flow: wrap;
gap: 10px;
}
.season-item,
.episode-item {
width: 150px;
cursor: pointer;
}
.episode-item {
mat-card-content {
padding: 0;
}
mat-card-title {
padding: 10px;
}
.episode-title {
padding: 5px;
}
.episode-cover {
width: 100%;
}
}
@@ -0,0 +1,27 @@
import { JsonPipe, KeyValuePipe, NgFor, NgIf } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { XtreamSerieEpisode } from '../../../../shared/xtream-serie-details.interface';
@Component({
selector: 'app-season-container',
templateUrl: './season-container.component.html',
styleUrls: ['./season-container.component.scss'],
standalone: true,
imports: [JsonPipe, KeyValuePipe, NgFor, MatCardModule, NgIf],
})
export class SeasonContainerComponent {
@Input({ required: true }) seasons: Record<string, XtreamSerieEpisode[]>;
@Output() episodeClicked = new EventEmitter<any>();
selectedSeason: string;
selectSeason(seasonKey: string) {
this.selectedSeason = seasonKey;
}
selectEpisode(episode: any) {
this.episodeClicked.emit(episode);
}
}
@@ -0,0 +1,49 @@
<div
class="container"
[style.background]="
item.info?.backdrop_path && item.info.backdrop_path.length > 0
? 'linear-gradient(to top, rgba(29,29,29,1) 0%, rgba(0,0,0,0.5) 100%), url(' +
item.info.backdrop_path[0] +
') repeat-x'
: 'none'
"
>
<div class="image">
<img *ngIf="item.info.cover" [src]="item.info.cover" />
</div>
<div class="details">
<h2>{{ item.info.name }}</h2>
<label *ngIf="item.info.plot">
{{ item.info.plot }}
</label>
<label *ngIf="item.info.releaseDate">
<div class="label">Release date:</div>
{{ item.info.releaseDate }}
</label>
<label *ngIf="item.info.genre">
<div class="label">Genre:</div>
{{ item.info.genre }}
</label>
<label *ngIf="item.info.rating">
<div class="label">Rating:</div>
{{ item.info.rating }}
</label>
<div *ngIf="item.info.cast">
<div class="label">Cast:</div>
{{ item.info.cast }}
</div>
<label *ngIf="item.info.director">
<div class="label">Director:</div>
{{ item.info.director }}
</label>
<label *ngIf="item.info.episode_run_time">
<div class="label">Episode run time:</div>
{{ item.info.episode_run_time }}
</label>
</div>
</div>
<app-season-container
[seasons]="item.episodes"
(episodeClicked)="playEpisode($event)"
/>
@@ -0,0 +1,38 @@
import { JsonPipe, NgIf } from '@angular/common';
import { Component, EventEmitter, Input, Output, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { OPEN_MPV_PLAYER } from '../../../../shared/ipc-commands';
import { XtreamSerieDetails } from '../../../../shared/xtream-serie-details.interface';
import { DataService } from '../../services/data.service';
import { selectCurrentPlaylist } from '../../state/selectors';
import { SeasonContainerComponent } from '../season-container/season-container.component';
@Component({
selector: 'app-serial-details',
templateUrl: './serial-details.component.html',
styleUrls: ['../detail-view.scss'],
standalone: true,
imports: [JsonPipe, MatButtonModule, NgIf, SeasonContainerComponent],
})
export class SerialDetailsComponent {
@Input({ required: true }) item: XtreamSerieDetails;
@Output() playClicked = new EventEmitter();
dialog = inject(MatDialog);
store = inject(Store);
dataService = inject(DataService);
currentPlaylist = this.store.selectSignal(selectCurrentPlaylist);
playEpisode(episode: any) {
const { serverUrl, username, password } = this.currentPlaylist();
const streamUrl = `${serverUrl}/series/${username}/${password}/${episode.id}.${episode.container_extension}`;
/* this.dialog.open(PlayerDialogComponent, {
data: streamUrl,
}); */
this.dataService.sendIpcEvent(OPEN_MPV_PLAYER, { url: streamUrl });
}
}
@@ -0,0 +1,49 @@
<div
class="container"
[style.background]="
item.info?.backdrop_path && item.info.backdrop_path.length > 0
? 'linear-gradient(to top, rgba(29,29,29,1) 0%, rgba(0,0,0,0.5) 100%), url(' +
item.info.backdrop_path[0] +
') repeat-x'
: 'none'
"
>
<div class="image">
<img *ngIf="item.info.movie_image" [src]="item.info.movie_image" />
</div>
<div class="details">
<h2>{{ item.info.name }}</h2>
<label *ngIf="item.info.description">
{{ item.info.description }}
</label>
<label *ngIf="item.info.releasedate">
<div class="label">Release date:</div>
{{ item.info.releasedate }}
</label>
<label *ngIf="item.info.genre">
<div class="label">Genre:</div>
{{ item.info.genre }}
</label>
<label *ngIf="item.info.country">
<div class="label">Country:</div>
{{ item.info.country }}
</label>
<div *ngIf="item.info.actors">
<div class="label">Actors:</div>
{{ item.info.actors }}
</div>
<label *ngIf="item.info.director">
<div class="label">Director:</div>
{{ item.info.director }}
</label>
<label *ngIf="item.info.duration">
<div class="label">Duration:</div>
{{ item.info.duration }}
</label>
<div class="action-buttons">
<button mat-flat-button color="accent" (click)="playClicked.emit()">
Play
</button>
</div>
</div>
</div>
@@ -0,0 +1,17 @@
import { JsonPipe, NgIf } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { XtreamVodDetails } from '../../../../shared/xtream-vod-details.interface';
@Component({
selector: 'app-vod-details',
templateUrl: './vod-details.component.html',
styleUrls: ['../detail-view.scss'],
standalone: true,
imports: [JsonPipe, MatButtonModule, NgIf],
})
export class VodDetailsComponent {
@Input({ required: true }) item: XtreamVodDetails;
@Output() playClicked = new EventEmitter<void>();
}
@@ -0,0 +1,33 @@
<app-navigation-bar
[breadcrumbs]="breadcrumbs"
[contentType]="selectedContentType"
(breadcrumbClicked)="breadcrumbClicked($event)"
(contentTypeChanged)="changeContentType($event)"
/>
<div class="container">
<app-category-view
*ngIf="currentLayout === 'category'"
(categoryClicked)="categoryClicked($event)"
[items]="items"
/>
<app-category-content-view
*ngIf="currentLayout === 'category_content'"
[items]="items"
(itemClicked)="itemClicked($event)"
/>
<app-vod-details
*ngIf="currentLayout === 'vod-details'"
[item]="vodDetails"
(playClicked)="playVod()"
/>
<app-serial-details
*ngIf="currentLayout === 'serie-details'"
[item]="vodDetails"
/>
<app-player-view *ngIf="currentLayout === 'player'" />
</div>
@@ -0,0 +1,4 @@
.container {
height: calc(100vh - 140px);
overflow-y: auto;
}
@@ -0,0 +1,326 @@
import { JsonPipe, KeyValuePipe, NgFor, NgIf, NgSwitch } from '@angular/common';
import { Component, NgZone, OnInit, effect, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Store } from '@ngrx/store';
import { IpcCommand } from '../../../shared/ipc-command.class';
import {
ERROR,
OPEN_MPV_PLAYER,
XTREAM_REQUEST,
XTREAM_RESPONSE,
} from '../../../shared/ipc-commands';
import { XtreamCategory } from '../../../shared/xtream-category.interface';
import { XtreamCodeActions } from '../../../shared/xtream-code-actions';
import { XtreamLiveStream } from '../../../shared/xtream-live-stream.interface';
import { XtreamResponse } from '../../../shared/xtream-response.interface';
import { XtreamVodDetails } from '../../../shared/xtream-vod-details.interface';
import { DataService } from '../services/data.service';
import { selectCurrentPlaylist } from '../state/selectors';
import { CategoryContentViewComponent } from './category-content-view/category-content-view.component';
import { CategoryViewComponent } from './category-view/category-view.component';
import { ContentType } from './content-type.enum';
import { NavigationBarComponent } from './navigation-bar/navigation-bar.component';
import { PlayerViewComponent } from './player-view/player-view.component';
import { VodDetailsComponent } from './vod-details/vod-details.component';
import { XtreamSerieDetails } from '../../../shared/xtream-serie-details.interface';
import { SerialDetailsComponent } from './serial-details/serial-details.component';
const ContentTypes = {
LIVE_STREAMS: {
title: 'Live streams',
getContentAction: XtreamCodeActions.GetLiveStreams,
getCategoryAction: XtreamCodeActions.GetLiveCategories,
},
VODS: {
title: 'VOD streams',
getContentAction: XtreamCodeActions.GetVodStreams,
getCategoryAction: XtreamCodeActions.GetVodCategories,
getDetailsAction: XtreamCodeActions.GetVodInfo,
},
SERIES: {
title: 'Series',
getContentAction: XtreamCodeActions.GetSeries,
getCategoryAction: XtreamCodeActions.GetSeriesCategories,
getDetailsAction: XtreamCodeActions.GetSeriesInfo,
},
};
export interface Breadcrumb {
action: XtreamCodeActions;
title: string;
category_id?: string;
}
type LayoutView =
| 'category'
| 'category_content'
| 'vod-details'
| 'player'
| 'serie-details';
@Component({
selector: 'app-xtream-main-container',
templateUrl: './xtream-main-container.component.html',
styleUrls: ['./xtream-main-container.component.scss'],
standalone: true,
imports: [
KeyValuePipe,
MatButtonToggleModule,
NgFor,
NgIf,
JsonPipe,
CategoryViewComponent,
NgSwitch,
MatButtonModule,
MatCardModule,
MatIconModule,
NavigationBarComponent,
VodDetailsComponent,
PlayerViewComponent,
CategoryContentViewComponent,
SerialDetailsComponent,
],
})
export class XtreamMainContainerComponent implements OnInit {
dataService = inject(DataService);
ngZone = inject(NgZone);
snackBar = inject(MatSnackBar);
store = inject(Store);
currentPlaylist = this.store.selectSignal(selectCurrentPlaylist);
breadcrumbs: Breadcrumb[] = [];
items = [];
listeners = [];
selectedContentType = ContentType.VODS;
currentLayout: LayoutView = 'category';
vodDetails!: XtreamVodDetails | XtreamSerieDetails;
commandsList = [
new IpcCommand(XTREAM_RESPONSE, (response: XtreamResponse) =>
this.handleResponse(response)
),
new IpcCommand(ERROR, (response: { message: string; status: number }) =>
this.showErrorAsNotification(response)
),
];
constructor() {
effect(() => {
if (this.currentPlaylist()) {
this.getCategories(this.selectedContentType);
}
});
}
ngOnInit() {
this.setInitialBreadcrumb();
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);
}
});
}
handleResponse(response: XtreamResponse) {
console.log(response.payload);
switch (response.action) {
case XtreamCodeActions.GetSeriesCategories:
case XtreamCodeActions.GetVodCategories:
case XtreamCodeActions.GetLiveCategories:
case XtreamCodeActions.GetVodStreams:
case XtreamCodeActions.GetLiveStreams:
case XtreamCodeActions.GetSeries:
this.items = response.payload as unknown[];
break;
case XtreamCodeActions.GetVodInfo:
this.currentLayout = 'vod-details';
this.vodDetails = response.payload as XtreamVodDetails;
break;
case XtreamCodeActions.GetSeriesInfo:
this.currentLayout = 'serie-details';
this.vodDetails = response.payload as XtreamSerieDetails;
break;
default:
break;
}
}
getCategories(contentType: ContentType = this.selectedContentType) {
this.currentLayout = 'category';
const action = ContentTypes[contentType].getCategoryAction;
this.setInitialBreadcrumb(action);
this.sendRequest({ action });
}
// TODO: MAKE IT ABSTRACT SOMEHOW
ngOnDestroy(): void {
if (this.dataService.isElectron) {
this.commandsList.forEach((command) =>
this.dataService.removeAllListeners(command.id)
);
} else {
this.listeners.forEach((listener) => {
window.removeEventListener('message', listener);
});
}
}
setInitialBreadcrumb(action = XtreamCodeActions.GetVodCategories) {
this.breadcrumbs = [{ title: 'All categories', action }];
}
categoryClicked(item: XtreamCategory) {
this.items = [];
const action = ContentTypes[this.selectedContentType].getContentAction;
this.breadcrumbs.push({
title: item.category_name,
category_id: item.category_id,
action,
});
this.sendRequest({ action, category_id: item.category_id });
this.currentLayout = 'category_content';
}
itemClicked(
item: any /* XtreamLiveStream | XtreamVodStream | XtreamSerieItem */
) {
let action;
this.items = [];
if (item.stream_type && item.stream_type === 'movie') {
action = XtreamCodeActions.GetVodInfo;
this.breadcrumbs.push({ title: item.name, action });
this.sendRequest({ action, vod_id: item.stream_id });
} else if (item.stream_type && item.stream_type === 'live') {
this.breadcrumbs.push({
title: item.name,
action: XtreamCodeActions.GetLiveStreams,
});
this.playStream(item);
} else if (item.series_id) {
action = XtreamCodeActions.GetSeriesInfo;
this.breadcrumbs.push({ title: item.name, action });
this.sendRequest({ action, series_id: item.series_id });
}
}
playStream(item: XtreamLiveStream) {
this.currentLayout = 'player';
const { serverUrl, username, password } = this.currentPlaylist();
const streamUrl = `${serverUrl}/${item.stream_type}/${username}/${password}/${item.stream_id}.ts`;
this.dataService.sendIpcEvent(OPEN_MPV_PLAYER, {
url: streamUrl,
});
}
playVod() {
this.currentLayout = 'player';
const { serverUrl, username, password } = this.currentPlaylist();
this.items = [];
const streamUrl = `${serverUrl}/movie/${username}/${password}/${
(this.vodDetails as XtreamVodDetails).movie_data.stream_id
}.${
(this.vodDetails as XtreamVodDetails).movie_data.container_extension
}`;
this.dataService.sendIpcEvent(OPEN_MPV_PLAYER, {
url: streamUrl,
});
}
changeContentType(contentType: ContentType) {
this.selectedContentType = contentType;
this.getCategories();
}
showErrorAsNotification(response: { message: string; status: number }) {
this.snackBar.open(
`Error: ${response?.message ?? 'Something went wrong'} (Status: ${
response?.status ?? 0
})`,
null,
{ duration: 4000 }
);
}
/**
* Should get the position of the clicked breadcrumb and remove all the items after
* @param breadcrumb clicked breadcrumb item
*/
breadcrumbClicked(breadcrumb: Breadcrumb) {
this.items = [];
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
);
this.currentLayout = this.getLayoutViewBasedOnAction(breadcrumb.action);
this.sendRequest({
action: breadcrumb.action,
...(breadcrumb.category_id
? { category_id: breadcrumb.category_id }
: {}),
});
}
getLayoutViewBasedOnAction(action: XtreamCodeActions) {
let result: LayoutView = 'category';
switch (action) {
case XtreamCodeActions.GetLiveCategories:
case XtreamCodeActions.GetVodCategories:
case XtreamCodeActions.GetSeriesCategories:
result = 'category';
break;
case XtreamCodeActions.GetLiveStreams:
case XtreamCodeActions.GetVodStreams:
case XtreamCodeActions.GetSeries:
result = 'category_content';
break;
case XtreamCodeActions.GetVodInfo:
case XtreamCodeActions.GetSeriesInfo:
result = 'vod-details';
break;
default:
console.error(`Error: Unknown action was provided: ${action}`);
break;
}
return result;
}
sendRequest(params: Record<string, string | number>) {
const { serverUrl, username, password } = this.currentPlaylist();
this.dataService.sendIpcEvent(XTREAM_REQUEST, {
url: serverUrl,
params: {
password,
username,
...params,
},
});
}
}
+43
View File
@@ -0,0 +1,43 @@
import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { XtreamCategory } from '../../../shared/xtream-category.interface';
export interface XtreamState {
liveStreams: any[];
series: any[];
vods: any[];
vodCategories: XtreamCategory[];
}
@Injectable({ providedIn: 'root' })
export class XtreamStore extends ComponentStore<XtreamState> {
constructor() {
super({ liveStreams: [], series: [], vods: [], vodCategories: [] });
}
readonly liveStreams = this.selectSignal((state) => state.liveStreams);
readonly series = this.selectSignal((state) => state.series);
readonly vods = this.selectSignal((state) => state.vods);
readonly vodCategories = this.selectSignal((state) => state.vodCategories);
readonly setLiveStreams = this.updater(
(state, liveStreams: any[]): XtreamState => ({
...state,
liveStreams,
})
);
readonly setSerials = this.updater(
(state, series: any[]): XtreamState => ({
...state,
series,
})
);
readonly setVods = this.updater(
(state, vods: any[]): XtreamState => ({
...state,
vods,
})
);
}