refactor: add xtream api services

This commit is contained in:
4gray committed 2026-01-11 06:25:12 +01:00
1 parent 97a678a98d
commit aa27e6da0d
3 files changed
+336

No files matched your search

@@ -0,0 +1,2 @@
export * from './xtream-api.service';
export * from './xtream-url.service';
@@ -0,0 +1,259 @@
import { inject, Injectable } from '@angular/core';
import { DataService } from 'services';
import {
EpgItem,
XtreamCategory,
XtreamCodeActions,
XtreamLiveStream,
XtreamSerieDetails,
XtreamSerieItem,
XtreamVodDetails,
XtreamVodStream,
} from 'shared-interfaces';
import { XtreamAccountInfo } from '../account-info/account-info.interface';
/**
* Xtream API credentials
*/
export interface XtreamCredentials {
serverUrl: string;
username: string;
password: string;
}
/**
* Category type mapping for API calls
*/
export type CategoryType = 'live' | 'vod' | 'series';
/**
* Stream type mapping for API calls
*/
export type StreamType = 'live' | 'movie' | 'series';
/**
* Raw EPG listing from API (before decoding)
*/
interface RawEpgListing {
title: string;
description: string;
start: string;
end: string;
start_timestamp: string;
stop_timestamp: string;
[key: string]: unknown;
}
/**
* EPG API response
*/
interface EpgResponse {
epg_listings?: RawEpgListing[];
}
/**
* Service for all Xtream Codes API interactions.
* Extracts API communication logic from the XtreamStore.
*/
@Injectable({ providedIn: 'root' })
export class XtreamApiService {
private readonly dataService = inject(DataService);
/**
* Get account/portal status
*/
async getAccountInfo(
credentials: XtreamCredentials
): Promise<XtreamAccountInfo> {
return this.dataService.fetchData(
`${credentials.serverUrl}/player_api.php`,
{
username: credentials.username,
password: credentials.password,
action: 'get_account_info',
}
);
}
/**
* Get categories by type
*/
async getCategories(
credentials: XtreamCredentials,
type: CategoryType
): Promise<XtreamCategory[]> {
const actionMap: Record<CategoryType, XtreamCodeActions> = {
live: XtreamCodeActions.GetLiveCategories,
vod: XtreamCodeActions.GetVodCategories,
series: XtreamCodeActions.GetSeriesCategories,
};
const response = await this.dataService.fetchData(
`${credentials.serverUrl}/player_api.php`,
{
action: actionMap[type],
username: credentials.username,
password: credentials.password,
}
);
return Array.isArray(response) ? response : [];
}
/**
* Get live streams
*/
async getLiveStreams(
credentials: XtreamCredentials
): Promise<XtreamLiveStream[]> {
const response = await this.dataService.fetchData(
`${credentials.serverUrl}/player_api.php`,
{
action: XtreamCodeActions.GetLiveStreams,
username: credentials.username,
password: credentials.password,
}
);
return Array.isArray(response) ? response : [];
}
/**
* Get VOD streams
*/
async getVodStreams(
credentials: XtreamCredentials
): Promise<XtreamVodStream[]> {
const response = await this.dataService.fetchData(
`${credentials.serverUrl}/player_api.php`,
{
action: XtreamCodeActions.GetVodStreams,
username: credentials.username,
password: credentials.password,
}
);
return Array.isArray(response) ? response : [];
}
/**
* Get series items
*/
async getSeriesStreams(
credentials: XtreamCredentials
): Promise<XtreamSerieItem[]> {
const response = await this.dataService.fetchData(
`${credentials.serverUrl}/player_api.php`,
{
action: XtreamCodeActions.GetSeries,
username: credentials.username,
password: credentials.password,
}
);
return Array.isArray(response) ? response : [];
}
/**
* Get streams by type (unified method)
*/
async getStreams(
credentials: XtreamCredentials,
type: StreamType
): Promise<XtreamLiveStream[] | XtreamVodStream[] | XtreamSerieItem[]> {
switch (type) {
case 'live':
return this.getLiveStreams(credentials);
case 'movie':
return this.getVodStreams(credentials);
case 'series':
return this.getSeriesStreams(credentials);
}
}
/**
* Get VOD details
*/
async getVodInfo(
credentials: XtreamCredentials,
vodId: string | number
): Promise<XtreamVodDetails> {
return this.dataService.fetchData(
`${credentials.serverUrl}/player_api.php`,
{
action: XtreamCodeActions.GetVodInfo,
username: credentials.username,
password: credentials.password,
vod_id: vodId,
}
);
}
/**
* Get series details
*/
async getSeriesInfo(
credentials: XtreamCredentials,
seriesId: string | number
): Promise<XtreamSerieDetails> {
return this.dataService.fetchData(
`${credentials.serverUrl}/player_api.php`,
{
action: XtreamCodeActions.GetSeriesInfo,
username: credentials.username,
password: credentials.password,
series_id: seriesId,
}
);
}
/**
* Get short EPG for a stream
* Returns decoded EPG items (title and description are base64 decoded)
*/
async getShortEpg(
credentials: XtreamCredentials,
streamId: number,
limit: number = 10
): Promise<EpgItem[]> {
const response: EpgResponse = await this.dataService.fetchData(
`${credentials.serverUrl}/player_api.php`,
{
action: XtreamCodeActions.GetShortEpg,
username: credentials.username,
password: credentials.password,
stream_id: streamId,
limit,
}
);
if (!response?.epg_listings || !Array.isArray(response.epg_listings)) {
return [];
}
return response.epg_listings.map((item) => ({
...item,
title: this.decodeBase64Unicode(item.title).trim(),
description: this.decodeBase64Unicode(item.description).trim(),
})) as EpgItem[];
}
/**
* Decode base64 unicode string (used for EPG data)
*/
private decodeBase64Unicode(str: string): string {
try {
return decodeURIComponent(
Array.prototype.map
.call(atob(str), (c: string) => {
return (
'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
);
})
.join('')
);
} catch {
return str;
}
}
}
@@ -0,0 +1,75 @@
import { inject, Injectable } from '@angular/core';
import {
XtreamSerieEpisode,
XtreamVodDetails,
} from 'shared-interfaces';
import { SettingsStore } from '../../services/settings-store.service';
import { XtreamCredentials } from './xtream-api.service';
/**
* Extended playlist with optional HTTP headers
*/
export interface XtreamPlaylistWithHeaders extends XtreamCredentials {
id: string;
name: string;
type?: string;
userAgent?: string;
referrer?: string;
origin?: string;
}
/**
* Live stream item with xtream_id
*/
export interface LiveStreamItem {
xtream_id: number;
[key: string]: unknown;
}
/**
* Service for constructing Xtream stream URLs.
* Handles URL construction for live streams, VOD, and series episodes.
*/
@Injectable({ providedIn: 'root' })
export class XtreamUrlService {
private readonly settingsStore = inject(SettingsStore);
/**
* Construct live stream URL
* Format: {serverUrl}/live/{username}/{password}/{streamId}.{format}
*/
constructLiveUrl(
credentials: XtreamCredentials,
xtreamId: number,
format?: string
): string {
const streamFormat =
format ?? this.settingsStore.streamFormat() ?? 'ts';
return `${credentials.serverUrl}/live/${credentials.username}/${credentials.password}/${xtreamId}.${streamFormat}`;
}
/**
* Construct VOD stream URL
* Format: {serverUrl}/movie/{username}/{password}/{streamId}.{extension}
*/
constructVodUrl(
credentials: XtreamCredentials,
vodItem: XtreamVodDetails
): string {
const streamId =
vodItem.movie_data.stream_id ?? (vodItem as any).stream_id;
const extension = vodItem.movie_data.container_extension;
return `${credentials.serverUrl}/movie/${credentials.username}/${credentials.password}/${streamId}.${extension}`;
}
/**
* Construct series episode stream URL
* Format: {serverUrl}/series/{username}/{password}/{episodeId}.{extension}
*/
constructEpisodeUrl(
credentials: XtreamCredentials,
episode: XtreamSerieEpisode
): string {
return `${credentials.serverUrl}/series/${credentials.username}/${credentials.password}/${episode.id}.${episode.container_extension}`;
}
}