Proyecto App Linux

This commit is contained in:
2026-05-03 23:43:09 +02:00
commit 7482733843
1248 changed files with 189750 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
import baseConfig from '../../../eslint.config.mjs';
export default [
...baseConfig,
{
files: ['**/*.json'],
rules: {
'@nx/dependency-checks': [
'error',
{
ignoredFiles: [
'{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
],
},
],
},
languageOptions: {
parser: await import('jsonc-eslint-parser'),
},
},
];
+13
View File
@@ -0,0 +1,13 @@
export default {
displayName: 'shared-interfaces',
preset: '../../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': [
'ts-jest',
{ tsconfig: '<rootDir>/tsconfig.spec.json' },
],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../coverage/libs/shared/interfaces',
};
+11
View File
@@ -0,0 +1,11 @@
{
"name": "shared-interfaces",
"version": "0.0.1",
"private": true,
"type": "commonjs",
"main": "./src/index.js",
"types": "./src/index.d.ts",
"dependencies": {
"tslib": "^2.3.0"
}
}
+27
View File
@@ -0,0 +1,27 @@
{
"name": "shared-interfaces",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/shared/interfaces/src",
"projectType": "library",
"tags": ["scope:shared", "domain:shared-contracts", "type:util"],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/shared/interfaces",
"main": "libs/shared/interfaces/src/index.ts",
"tsConfig": "libs/shared/interfaces/tsconfig.lib.json",
"assets": []
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/shared/interfaces/jest.config.ts",
"tsConfig": "libs/shared/interfaces/tsconfig.spec.json"
}
}
}
}
+50
View File
@@ -0,0 +1,50 @@
export * from './lib/channel.interface';
export * from './lib/channel.model';
export * from './lib/epg-channel-metadata.model';
export * from './lib/epg-channel-with-programs.interface';
export * from './lib/epg-channel.model';
export * from './lib/epg-item.interface';
export * from './lib/epg-program.model';
export * from './lib/external-player-session.interface';
export * from './lib/indexed-db.config';
export * from './lib/ipc-command.class';
export * from './lib/ipc-commands';
export * from './lib/language.enum';
export * from './lib/parsed-playlist.interface';
export * from './lib/playback-position.interface';
export * from './lib/playlist-backup.interface';
export * from './lib/playlist-meta.type';
export * from './lib/playlist-recently-viewed.interface';
export * from './lib/playlist-recently-viewed.utils';
export * from './lib/playlist-refresh.interface';
export * from './lib/playlist.interface';
export * from './lib/portal-activity-item.interface';
export * from './lib/portal-debug.interface';
export * from './lib/portal-playback.interface';
export * from './lib/settings.interface';
export * from './lib/stalker-portal-actions.enum';
export * from './lib/store-keys.enum';
export * from './lib/stream-format.enum';
export * from './lib/theme.enum';
export * from './lib/xtream-account-info-dialog-data.interface';
export * from './lib/xtream-category.interface';
export * from './lib/xtream-code-actions';
export * from './lib/xtream-item.interface';
export * from './lib/xtream-live-stream.interface';
export * from './lib/xtream-response.interface';
export * from './lib/xtream-restore-state.util';
export * from './lib/xtream-serie-details.interface';
export * from './lib/xtream-serie-item.interface';
export * from './lib/xtream-vod-details.interface';
export * from './lib/xtream-vod-stream.interface';
export * from './lib/workspace-product-branding.interface';
// Stalker interfaces
export * from './lib/stalker-item.normalizer';
export * from './lib/stalker-portal-item.interface';
export * from './lib/stalker-serial-details.interface';
export * from './lib/stalker-vod-details.interface';
// Unified VOD details (discriminated union + adapters)
export * from './lib/vod-details-adapters';
export * from './lib/vod-details-item.interface';
@@ -0,0 +1,32 @@
/**
* Represents channel object
* TODO: define channel interface in iptv-parser library
*/
export interface Channel {
id: string;
url: string;
name: string;
group: {
title: string;
};
tvg: {
id: string;
name: string;
url: string;
logo: string;
rec: string;
};
epgParams?: string;
timeshift?: string;
catchup?: {
type?: string;
source?: string;
days?: string;
};
http: {
referrer: string;
'user-agent': string;
origin: string;
};
radio: string;
}
+22
View File
@@ -0,0 +1,22 @@
import { Channel } from './channel.interface';
/**
* Creates new channel object based on the given fields
* @param params partial channel object
*/
export function createChannel(params: Partial<Channel>): Channel {
return {
id: params?.id || params.url,
name: params.name,
group: params.group,
url: params.url,
tvg: params.tvg,
timeshift: params.timeshift,
catchup: params.catchup,
http: params.http,
radio: params.radio,
} as Channel;
}
/** Possible sidebar view options */
export type SidebarView = 'CHANNELS' | 'PLAYLISTS';
@@ -0,0 +1,9 @@
/**
* Flattened EPG channel metadata returned to the renderer for lookup-based
* enrichment of playlist channels.
*/
export interface EpgChannelMetadata {
id: string;
displayName: string;
iconUrl: string | null;
}
@@ -0,0 +1,17 @@
import { EpgChannel } from './epg-channel.model';
import { EpgProgram } from './epg-program.model';
/**
* EPG Channel with associated programs
*/
export interface EpgChannelWithPrograms extends EpgChannel {
programs: EpgProgram[];
}
/**
* EPG Data structure
*/
export interface EpgData {
channels: EpgChannel[];
programs: EpgProgram[];
}
@@ -0,0 +1,9 @@
/**
* EPG Channel interface compatible with epg-parser v0.4.0
*/
export interface EpgChannel {
id: string;
displayName: { lang: string; value: string }[];
icon: { src: string; width?: number; height?: number }[];
url: string[];
}
@@ -0,0 +1,13 @@
export interface EpgItem {
id: string;
epg_id: string;
title: string;
lang: string;
start: string;
end: string;
stop: string;
description: string;
channel_id: string;
start_timestamp: string;
stop_timestamp: string;
}
@@ -0,0 +1,17 @@
/**
* EPG Program interface - flat structure matching database storage
*/
export interface EpgProgram {
start: string; // ISO string
stop: string; // ISO string
channel: string;
title: string;
desc: string | null;
category: string | null;
startTimestamp?: number | null;
stopTimestamp?: number | null;
date?: string;
episodeNum?: string | null;
iconUrl?: string | null;
rating?: string | null;
}
@@ -0,0 +1,24 @@
import { PlayerContentInfo } from './portal-playback.interface';
export type ExternalPlayerName = 'mpv' | 'vlc';
export type ExternalPlayerSessionStatus =
| 'launching'
| 'opened'
| 'playing'
| 'error'
| 'closed';
export interface ExternalPlayerSession {
id: string;
player: ExternalPlayerName;
status: ExternalPlayerSessionStatus;
title: string;
thumbnail?: string | null;
streamUrl: string;
contentInfo?: PlayerContentInfo;
startedAt: string;
updatedAt: string;
error?: string;
canClose: boolean;
}
@@ -0,0 +1,74 @@
import { DBConfig } from 'ngx-indexed-db';
/**
* Contains names of the database stores
*/
export enum DbStores {
Playlists = 'playlists',
}
/** Defines db tables and schema */
export const dbConfig: DBConfig = {
name: 'iptvnator',
version: 1,
objectStoresMeta: [
{
store: DbStores.Playlists,
storeConfig: { keyPath: '_id', autoIncrement: false },
storeSchema: [
{
name: '_id',
keypath: '_id',
options: { unique: false },
},
{
name: 'filename',
keypath: 'filename',
options: { unique: false },
},
{ name: 'title', keypath: 'title', options: { unique: false } },
{ name: 'count', keypath: 'count', options: { unique: false } },
{
name: 'playlist',
keypath: 'playlist',
options: { unique: false },
},
{
name: 'importDate',
keypath: 'importDate',
options: { unique: false },
},
{
name: 'lastUsage',
keypath: 'lastUsage',
options: { unique: false },
},
{
name: 'favorites',
keypath: 'favorites',
options: { unique: false },
},
{
name: 'recentlyViewed',
keypath: 'recentlyViewed',
options: { unique: false },
},
{
name: 'autoRefresh',
keypath: 'autoRefresh',
options: { unique: false },
},
{
name: 'url',
keypath: 'url',
options: { unique: false },
},
{
name: 'filePath',
keypath: 'filePath',
options: { unique: false },
},
],
},
],
};
@@ -0,0 +1,9 @@
export class IpcCommand {
constructor(
public id: string,
public callback: (payload: any) => void
) {
this.id = id;
this.callback = callback;
}
}
@@ -0,0 +1,56 @@
// EPG related commands
export const EPG_FETCH = 'EPG:FETCH';
export const EPG_FETCH_DONE = 'EPG:FETCH_DONE';
export const EPG_ERROR = 'EPG:ERROR';
export const EPG_GET_CHANNELS = 'EPG:GET_CHANNELS';
export const EPG_GET_CHANNELS_DONE = 'EPG:GET_CHANNELS_DONE';
export const EPG_GET_CHANNELS_BY_RANGE = 'EPG:EPG_GET_CHANNELS_BY_RANGE';
export const EPG_GET_CHANNELS_BY_RANGE_RESPONSE =
'EPG:EPG_GET_CHANNELS_BY_RANGE_RESPONSE';
export const EPG_FORCE_FETCH = 'EPG:EPG_FORCE_FETCH';
// Playlist related commands
export const PLAYLIST_PARSE_BY_URL = 'PLAYLIST:PARSE_PLAYLIST_BY_URL';
export const PLAYLIST_UPDATE = 'PLAYLIST:UPDATE';
export const PLAYLIST_REFRESH = 'PLAYLIST:REFRESH';
export const PLAYLIST_REFRESH_EVENT = 'PLAYLIST:REFRESH_EVENT';
export const PLAYLIST_CANCEL_REFRESH = 'PLAYLIST:CANCEL_REFRESH';
// General
export const ERROR = 'ERROR';
export const OPEN_FILE = 'OPEN_FILE';
// Views
export const VIEW_SETTINGS = 'VIEW:SETTINGS';
export const VIEW_ADD_PLAYLIST = 'VIEW:PLAYLISTS';
// Auto-update
export const AUTO_UPDATE_PLAYLISTS = 'AUTO_UPDATE';
export const AUTO_UPDATE_PLAYLISTS_RESPONSE = 'AUTO_UPDATE_RESPONSE';
// Experimental
export const OPEN_MPV_PLAYER = 'OPEN_MPV_PLAYER';
export const SET_MPV_PLAYER_PATH = 'SET_MPV_PLAYER_PATH';
export const SET_MPV_REUSE_INSTANCE = 'SET_MPV_REUSE_INSTANCE';
export const OPEN_VLC_PLAYER = 'OPEN_VLC_PLAYER';
export const SET_VLC_PLAYER_PATH = 'SET_VLC_PLAYER_PATH';
export const CLOSE_EXTERNAL_PLAYER_SESSION = 'CLOSE_EXTERNAL_PLAYER_SESSION';
export const EXTERNAL_PLAYER_SESSION_UPDATE =
'EXTERNAL_PLAYER_SESSION_UPDATE';
// Xtream
export const XTREAM_REQUEST = 'XTREAM_REQUEST';
export const XTREAM_RESPONSE = 'XTREAM_RESPONSE';
export const XTREAM_CANCEL_SESSION = 'XTREAM_CANCEL_SESSION';
// Stalker
export const STALKER_REQUEST = 'STALKER_REQUEST';
export const STALKER_RESPONSE = 'STALKER_RESPONSE';
export const PORTAL_DEBUG_EVENT = 'PORTAL_DEBUG_EVENT';
// Settings
export const SETTINGS_UPDATE = 'SETTINGS_UPDATE';
export const DELETE_ALL_PLAYLISTS = 'DELETE_ALL_PLAYLISTS';
// Remote Control
export const REMOTE_CONTROL_CHANGE_CHANNEL = 'REMOTE_CONTROL_CHANGE_CHANNEL';
@@ -0,0 +1,23 @@
/**
* Enum with all languages that are available in the application
*/
export enum Language {
ARABIC = 'ar',
MOROCCAN_ARABIC = 'ary',
ENGLISH = 'en',
KOREAN = 'ko',
RUSSIAN = 'ru',
GERMAN = 'de',
SPANISH = 'es',
CHINESE = 'zh',
TRADITIONAL_CHINESE = 'zhtw',
FRENCH = 'fr',
ITALIAN = 'it',
TURKISH = 'tr',
JAPANESE = 'ja',
DUTCH = 'nl',
BELARUSIAN = 'by',
POLISH = 'pl',
PORTUGUESE = 'pt',
GREEK = 'el',
}
@@ -0,0 +1,36 @@
export interface ParsedPlaylist {
header: {
attrs: {
'x-tvg-url': string;
};
raw: string;
};
items: ParsedPlaylistItem[];
}
export interface ParsedPlaylistItem {
name: string;
tvg: {
id: string;
name: string;
url: string;
logo: string;
rec: string;
};
group: {
title: string;
};
http: {
referrer: string;
'user-agent': string;
};
url: string;
raw: string;
catchup?: {
type?: string;
source?: string;
days?: string;
};
timeshift?: string;
radio?: string;
}
@@ -0,0 +1,15 @@
/**
* Playback position data for resume functionality
*/
export interface PlaybackPositionData {
contentXtreamId: number;
contentType: 'vod' | 'episode';
seriesXtreamId?: number;
seasonNumber?: number;
episodeNumber?: number;
positionSeconds: number;
durationSeconds?: number;
playlistId?: string;
updatedAt?: string;
sessionId?: string;
}
@@ -0,0 +1,113 @@
import { PlaybackPositionData } from './playback-position.interface';
import { M3uRecentlyViewedItem } from './playlist-recently-viewed.interface';
import { StalkerPortalItem } from './stalker-portal-item.interface';
export const PLAYLIST_BACKUP_KIND = 'iptvnator-playlist-backup';
export const PLAYLIST_BACKUP_VERSION = 1;
export type PlaylistBackupPortalType = 'm3u' | 'xtream' | 'stalker';
export type M3uBackupSourceKind = 'url' | 'file' | 'text';
export type XtreamBackupContentType = 'live' | 'movie' | 'series';
export type XtreamBackupCategoryType = 'live' | 'movies' | 'series';
export interface PlaylistBackupSettings {
epgUrls: string[];
}
export interface PlaylistBackupBaseEntry {
portalType: PlaylistBackupPortalType;
exportedId: string;
title: string;
autoRefresh: boolean;
position?: number;
}
export interface M3uPlaylistBackupEntry extends PlaylistBackupBaseEntry {
portalType: 'm3u';
source: {
kind: M3uBackupSourceKind;
rawM3u: string;
url?: string;
userAgent?: string;
referrer?: string;
origin?: string;
filePathHint?: string;
};
userState: {
favorites: string[];
recentlyViewed: M3uRecentlyViewedItem[];
hiddenGroupTitles: string[];
};
}
export interface XtreamBackupHiddenCategory {
categoryType: XtreamBackupCategoryType;
xtreamId: number;
}
export interface XtreamBackupFavoriteItem {
contentType: XtreamBackupContentType;
xtreamId: number;
addedAt?: string;
position?: number | null;
}
export interface XtreamBackupRecentlyViewedItem {
contentType: XtreamBackupContentType;
xtreamId: number;
viewedAt: string;
}
export interface XtreamBackupUserState {
hiddenCategories: XtreamBackupHiddenCategory[];
favorites: XtreamBackupFavoriteItem[];
recentlyViewed: XtreamBackupRecentlyViewedItem[];
playbackPositions: PlaybackPositionData[];
}
export interface XtreamPlaylistBackupEntry extends PlaylistBackupBaseEntry {
portalType: 'xtream';
connection: {
serverUrl: string;
username: string;
password?: string;
};
userState: XtreamBackupUserState;
}
export interface StalkerPlaylistBackupEntry extends PlaylistBackupBaseEntry {
portalType: 'stalker';
connection: {
portalUrl: string;
macAddress: string;
isFullStalkerPortal?: boolean;
username?: string;
password?: string;
userAgent?: string;
referrer?: string;
origin?: string;
stalkerSerialNumber?: string;
stalkerDeviceId1?: string;
stalkerDeviceId2?: string;
stalkerSignature1?: string;
stalkerSignature2?: string;
};
userState: {
favorites: StalkerPortalItem[];
recentlyViewed: StalkerPortalItem[];
};
}
export type PlaylistBackupEntry =
| M3uPlaylistBackupEntry
| XtreamPlaylistBackupEntry
| StalkerPlaylistBackupEntry;
export interface PlaylistBackupManifestV1 {
kind: typeof PLAYLIST_BACKUP_KIND;
version: typeof PLAYLIST_BACKUP_VERSION;
exportedAt: string;
includeSecrets: boolean;
settings?: PlaylistBackupSettings;
playlists: PlaylistBackupEntry[];
}
@@ -0,0 +1,33 @@
import { Playlist } from './playlist.interface';
export type PlaylistMeta = Pick<
Playlist,
| 'count'
| 'title'
| 'filename'
| '_id'
| 'url'
| 'importDate'
| 'userAgent'
| 'referrer'
| 'origin'
| 'filePath'
| 'updateDate'
| 'updateState'
| 'position'
| 'autoRefresh'
| 'favorites'
| 'serverUrl'
| 'username'
| 'password'
| 'macAddress'
| 'hiddenGroupTitles'
| 'portalUrl'
| 'recentlyViewed'
| 'isFullStalkerPortal'
| 'stalkerSerialNumber'
| 'stalkerDeviceId1'
| 'stalkerDeviceId2'
| 'stalkerSignature1'
| 'stalkerSignature2'
>;
@@ -0,0 +1,34 @@
import { StalkerPortalItem } from './stalker-portal-item.interface';
export interface M3uRecentlyViewedItem {
source: 'm3u';
id: string;
url: string;
title: string;
channel_id?: string;
poster_url?: string;
tvg_id?: string;
tvg_name?: string;
group_title?: string;
category_id: 'live';
added_at: string | number;
}
export type PlaylistRecentlyViewedItem =
| M3uRecentlyViewedItem
| StalkerPortalItem;
export function isM3uRecentlyViewedItem(
item: unknown
): item is M3uRecentlyViewedItem {
if (!item || typeof item !== 'object') {
return false;
}
const candidate = item as Record<string, unknown>;
return (
candidate['source'] === 'm3u' &&
typeof candidate['url'] === 'string' &&
candidate['url'].trim().length > 0
);
}
@@ -0,0 +1,117 @@
import { PlaylistMeta } from './playlist-meta.type';
import { PortalRecentItem } from './portal-activity-item.interface';
import {
extractStalkerItemId,
extractStalkerItemPoster,
extractStalkerItemTitle,
extractStalkerItemType,
normalizeStalkerDate,
} from './stalker-item.normalizer';
import {
PlaylistRecentlyViewedItem,
isM3uRecentlyViewedItem,
} from './playlist-recently-viewed.interface';
interface PlaylistRecentLabels {
stalker: string;
m3u: string;
}
function getPlaylistRecentItems(
playlist: PlaylistMeta
): PlaylistRecentlyViewedItem[] {
return Array.isArray(playlist.recentlyViewed)
? (playlist.recentlyViewed as PlaylistRecentlyViewedItem[])
: [];
}
function mapStalkerPlaylistRecentItems(
playlist: PlaylistMeta,
defaultPlaylistName: string
): PortalRecentItem[] {
return getPlaylistRecentItems(playlist).reduce<PortalRecentItem[]>(
(acc, rawItem, index) => {
if (isM3uRecentlyViewedItem(rawItem)) {
return acc;
}
const item = (rawItem ?? {}) as Record<string, unknown>;
const id = extractStalkerItemId(item, playlist._id, index);
acc.push({
id,
title: extractStalkerItemTitle(item),
type: extractStalkerItemType(item),
playlist_id: playlist._id,
playlist_name: playlist.title || defaultPlaylistName,
viewed_at: normalizeStalkerDate(item['added_at']),
category_id: String(item['category_id'] ?? ''),
xtream_id: id,
poster_url: extractStalkerItemPoster(item),
source: 'stalker',
stalker_item: rawItem,
});
return acc;
},
[]
);
}
function mapM3uPlaylistRecentItems(
playlist: PlaylistMeta,
defaultPlaylistName: string
): PortalRecentItem[] {
return getPlaylistRecentItems(playlist).reduce<PortalRecentItem[]>(
(acc, rawItem) => {
if (!isM3uRecentlyViewedItem(rawItem)) {
return acc;
}
const channelUrl = rawItem.url.trim();
if (!channelUrl) {
return acc;
}
acc.push({
id: rawItem.id || channelUrl,
title:
rawItem.title?.trim() ||
rawItem.tvg_name?.trim() ||
rawItem.channel_id ||
channelUrl,
type: 'live',
playlist_id: playlist._id,
playlist_name: playlist.title || defaultPlaylistName,
viewed_at: normalizeStalkerDate(rawItem.added_at),
category_id: rawItem.category_id || 'live',
xtream_id: channelUrl,
poster_url: rawItem.poster_url || undefined,
source: 'm3u',
});
return acc;
},
[]
);
}
export function buildPlaylistRecentItems(
playlists: PlaylistMeta[],
labels: PlaylistRecentLabels
): PortalRecentItem[] {
return playlists.reduce<PortalRecentItem[]>((acc, playlist) => {
if (playlist.macAddress) {
acc.push(
...mapStalkerPlaylistRecentItems(playlist, labels.stalker)
);
return acc;
}
if (!playlist.serverUrl) {
acc.push(...mapM3uPlaylistRecentItems(playlist, labels.m3u));
}
return acc;
}, []);
}
@@ -0,0 +1,28 @@
export type PlaylistRefreshPhase =
| 'fetching'
| 'reading-file'
| 'parsing'
| 'saving';
export type PlaylistRefreshStatus =
| 'started'
| 'progress'
| 'completed'
| 'cancelled'
| 'error';
export interface PlaylistRefreshEvent {
operationId: string;
playlistId: string;
phase?: PlaylistRefreshPhase;
status: PlaylistRefreshStatus;
error?: string;
}
export interface PlaylistRefreshPayload {
operationId: string;
playlistId: string;
title: string;
filePath?: string;
url?: string;
}
@@ -0,0 +1,70 @@
import { PlaylistRecentlyViewedItem } from './playlist-recently-viewed.interface';
import { StalkerPortalItem } from './stalker-portal-item.interface';
/**
* An interface that describe the possible states of the playlist update/refresh process
*/
export enum PlaylistUpdateState {
UPDATED,
IN_PROGRESS,
NOT_UPDATED,
}
/**
* Describes playlist interface
*/
export interface Playlist {
_id: string;
title: string;
filename?: string;
playlist?: any;
importDate: string;
lastUsage: string;
/**
* M3U playlists store channel URL strings (`string[]`).
* Stalker portals store full item objects (`StalkerPortalItem[]`).
*/
favorites?: (string | StalkerPortalItem)[];
items?: unknown[];
header?: unknown;
count: number;
url?: string;
userAgent?: string;
referrer?: string;
origin?: string;
filePath?: string;
autoRefresh: boolean;
updateDate?: number;
updateState?: PlaylistUpdateState;
position?: number;
isTemporary?: boolean;
serverUrl?: string;
username?: string;
password?: string;
macAddress?: string;
portalUrl?: string;
recentlyViewed?: PlaylistRecentlyViewedItem[];
/** Indicates if this is a full stalker portal URL (e.g., /stalker_portal/c) requiring handshake authentication */
isFullStalkerPortal?: boolean;
/** Session token for full stalker portal authentication - persisted for session */
stalkerToken?: string;
/** Serial number for stalker portal - generated once and stored for consistency */
stalkerSerialNumber?: string;
/** Optional device ID 1 for stalker portal - if not provided, auto-generated from MAC */
stalkerDeviceId1?: string;
/** Optional device ID 2 for stalker portal - if not provided, auto-generated from MAC */
stalkerDeviceId2?: string;
/** Optional signature 1 for stalker portal - required by some portals for device verification */
stalkerSignature1?: string;
/** Optional signature 2 for stalker portal - required by some portals for device verification */
stalkerSignature2?: string;
/** Account info from get_profile call */
stalkerAccountInfo?: {
login?: string;
expireDate?: number;
tariffPlanName?: string;
status?: number;
};
/** Hidden M3U group titles for the groups view */
hiddenGroupTitles?: string[];
}
@@ -0,0 +1,44 @@
import {
PortalActivityType,
StalkerPortalItem,
} from './stalker-portal-item.interface';
/**
* Base interface for cross-provider activity items (recently viewed,
* favorites) displayed on the dashboard and in global lists.
*/
export interface PortalActivityItem {
id: string | number;
title: string;
type: PortalActivityType;
playlist_id: string;
playlist_name?: string;
category_id: string | number;
xtream_id: string | number;
poster_url?: string;
/**
* Wide backdrop URL (typically 16:9), persisted when the user first views
* the detail page. Absent for stalker and for items that have never had
* their detail page opened. Dashboards should fall back to a blurred
* `poster_url` when undefined.
*/
backdrop_url?: string;
source?: 'xtream' | 'stalker' | 'm3u';
/** Original stalker item for navigation state; undefined for xtream. */
stalker_item?: StalkerPortalItem;
}
/** A recently-viewed item with a `viewed_at` timestamp. */
export interface PortalRecentItem extends PortalActivityItem {
viewed_at: string;
}
/** A favorite item with an `added_at` timestamp. */
export interface PortalFavoriteItem extends PortalActivityItem {
added_at: string;
}
/** A recently-added catalog item with an `added_at` timestamp. */
export interface PortalAddedItem extends PortalActivityItem {
added_at: string;
}
@@ -0,0 +1,21 @@
export type PortalDebugProvider = 'xtream' | 'stalker';
export type PortalDebugTransport =
| 'electron-main'
| 'electron-renderer'
| 'pwa-http';
export type PortalDebugStatus = 'success' | 'error';
export interface PortalDebugEvent {
requestId: string;
provider: PortalDebugProvider;
operation: string;
transport: PortalDebugTransport;
startedAt: string;
durationMs: number;
status: PortalDebugStatus;
request: unknown;
response?: unknown;
error?: unknown;
}
@@ -0,0 +1,21 @@
import { PlaybackPositionData } from './playback-position.interface';
export interface PlayerContentInfo
extends Omit<
PlaybackPositionData,
'positionSeconds' | 'durationSeconds' | 'updatedAt'
> {
playlistId: string;
}
export interface ResolvedPortalPlayback {
streamUrl: string;
title: string;
thumbnail?: string | null;
startTime?: number;
contentInfo?: PlayerContentInfo;
headers?: Record<string, string>;
userAgent?: string;
referer?: string;
origin?: string;
}
@@ -0,0 +1,42 @@
import { Language } from './language.enum';
import { StreamFormat } from './stream-format.enum';
import { Theme } from './theme.enum';
/**
* Contains all types of supported video players
*/
export enum VideoPlayer {
VideoJs = 'videojs',
Html5Player = 'html5',
MPV = 'mpv',
VLC = 'vlc',
ArtPlayer = 'artplayer',
}
export enum StartupBehavior {
FirstView = 'first-view',
RestoreLastView = 'restore-last-view',
}
/**
* Describes all available settings options of the application
*/
export interface Settings {
player: VideoPlayer;
epgUrl: string[];
streamFormat: StreamFormat;
language: Language;
showCaptions: boolean;
showDashboard: boolean;
startupBehavior: StartupBehavior;
/** Show the desktop footer bar for external playback status */
showExternalPlaybackBar?: boolean;
theme: Theme;
mpvPlayerPath: string;
mpvReuseInstance: boolean;
vlcPlayerPath: string;
remoteControl: boolean;
remoteControlPort: number;
/** Custom download folder path (uses system Downloads folder if not set) */
downloadFolder?: string;
}
@@ -0,0 +1,27 @@
import { normalizeStalkerDate } from './stalker-item.normalizer';
describe('normalizeStalkerDate', () => {
it('normalizes SQLite UTC timestamps without relying on engine-specific parsing', () => {
const originalParse = Date.parse;
const parseSpy = jest
.spyOn(Date, 'parse')
.mockImplementation((value: string) => {
if (value === '2026-04-21 22:49:02') {
return Number.NaN;
}
return originalParse(value);
});
expect(normalizeStalkerDate('2026-04-21 22:49:02')).toBe(
'2026-04-21T22:49:02.000Z'
);
expect(parseSpy).not.toHaveBeenCalledWith('2026-04-21 22:49:02');
});
it('preserves fractional seconds in SQLite UTC timestamps', () => {
expect(normalizeStalkerDate('2026-04-21 22:49:02.7')).toBe(
'2026-04-21T22:49:02.700Z'
);
});
});
@@ -0,0 +1,157 @@
import {
PortalActivityType,
StalkerPortalItem,
} from './stalker-portal-item.interface';
const SQLITE_UTC_TIMESTAMP_PATTERN =
/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?$/;
/**
* Extract a stable ID from an untyped Stalker portal item.
*
* Stalker portals use different id fields depending on the content type
* (`id`, `stream_id`, `series_id`, `movie_id`). This function returns
* the first non-empty one, or a generated fallback based on the playlist
* id and index.
*/
export function extractStalkerItemId(
item: StalkerPortalItem | Record<string, unknown>,
fallbackPrefix = '',
fallbackIndex = 0
): string {
const raw = item as Record<string, unknown>;
const id =
raw['id'] ?? raw['stream_id'] ?? raw['series_id'] ?? raw['movie_id'];
const idStr = String(id ?? '').trim();
return idStr || `${fallbackPrefix}-${fallbackIndex}`;
}
/**
* Extract a display title from a Stalker portal item.
*/
export function extractStalkerItemTitle(
item: StalkerPortalItem | Record<string, unknown>
): string {
const raw = item as Record<string, unknown>;
return String(raw['title'] ?? raw['o_name'] ?? raw['name'] ?? 'Unknown');
}
/**
* Extract a poster/cover URL from a Stalker portal item.
*/
export function extractStalkerItemPoster(
item: StalkerPortalItem | Record<string, unknown>
): string {
const raw = item as Record<string, unknown>;
return String(raw['cover'] ?? raw['logo'] ?? raw['poster_url'] ?? '');
}
/**
* Determine the normalised activity type of a Stalker item.
*
* - `itv` / `live` → `'live'`
* - `series` or `is_series` truthy → `'series'`
* - everything else → `'movie'`
*/
export function extractStalkerItemType(
item: StalkerPortalItem | Record<string, unknown>
): PortalActivityType {
const raw = item as Record<string, unknown>;
const categoryId = String(raw['category_id'] ?? '').toLowerCase();
const streamType = String(raw['stream_type'] ?? '').toLowerCase();
if (categoryId === 'itv' || streamType === 'live') {
return 'live';
}
if (
categoryId === 'series' ||
raw['is_series'] === true ||
raw['is_series'] === 1 ||
raw['is_series'] === '1'
) {
return 'series';
}
return 'movie';
}
/**
* Normalise a raw date value (epoch number, numeric string, or ISO string)
* into a consistent ISO 8601 string.
*/
export function normalizeStalkerDate(value: unknown): string {
if (typeof value === 'number') {
if (!Number.isFinite(value)) return '';
const ms = value > 10_000_000_000 ? value : value * 1000;
return new Date(ms).toISOString();
}
if (typeof value === 'string') {
const trimmed = value.trim();
if (/^\d+$/.test(trimmed)) {
const numeric = Number(trimmed);
if (Number.isFinite(numeric)) {
const ms = numeric > 10_000_000_000 ? numeric : numeric * 1000;
return new Date(ms).toISOString();
}
}
const sqliteUtcIso = normalizeSqliteUtcTimestamp(trimmed);
if (sqliteUtcIso) {
return sqliteUtcIso;
}
const parsed = Date.parse(trimmed);
if (!Number.isNaN(parsed)) {
return new Date(parsed).toISOString();
}
}
return '';
}
/**
* Check whether a raw item from `Playlist.favorites` matches the given itemId.
*
* Used to filter stalker favorites/recently-viewed arrays when removing items.
*/
export function stalkerItemMatchesId(
raw: unknown,
targetId: string,
fallbackPrefix = '',
fallbackIndex = 0
): boolean {
const item = (raw ?? {}) as Record<string, unknown>;
const rawId = extractStalkerItemId(item, fallbackPrefix, fallbackIndex);
return rawId === targetId;
}
function normalizeSqliteUtcTimestamp(value: string): string | null {
const match = SQLITE_UTC_TIMESTAMP_PATTERN.exec(value);
if (!match) {
return null;
}
const [
,
year,
month,
day,
hours,
minutes,
seconds,
milliseconds = '0',
] = match;
const normalizedMs = milliseconds.padEnd(3, '0').slice(0, 3);
return new Date(
Date.UTC(
Number(year),
Number(month) - 1,
Number(day),
Number(hours),
Number(minutes),
Number(seconds),
Number(normalizedMs)
)
).toISOString();
}
@@ -0,0 +1,11 @@
export enum StalkerPortalActions {
GetCategories = 'get_categories',
GetGenres = 'get_genres',
CreateLink = 'create_link',
GetOrderedList = 'get_ordered_list',
Favorites = 'favorites',
Handshake = 'handshake',
DoAuth = 'do_auth',
GetShortEpg = 'get_short_epg',
GetEpgInfo = 'get_epg_info',
}
@@ -0,0 +1,31 @@
/**
* Represents a Stalker portal item stored in `Playlist.favorites` or
* `Playlist.recentlyViewed` arrays.
*
* Stalker portals store favourites / recently-viewed as full objects
* (unlike M3U playlists, which use a simple `string[]` of channel URLs).
*/
export interface StalkerPortalItem {
id?: string | number;
stream_id?: string | number;
series_id?: string | number;
movie_id?: string | number;
title?: string;
/** Original name */
o_name?: string;
name?: string;
cover?: string;
logo?: string;
poster_url?: string;
/** 'itv' | 'vod' | 'series' — Stalker content type */
category_id?: string | number;
stream_type?: string;
is_series?: boolean | number | string;
/** Embedded VOD-series episode numbers preserved for VOD favorites. */
series?: unknown[];
added_at?: string | number;
cmd?: string;
}
/** Activity type normalised across all providers. */
export type PortalActivityType = 'live' | 'movie' | 'series';
@@ -0,0 +1,28 @@
/**
* Stalker portal series/serial details response structure.
* Contains series metadata from Stalker/Ministra portals.
*/
export interface StalkerSerialDetails {
/** Series title */
name: string;
/** Plot description */
description: string;
/** Director name */
director: string;
/** Comma-separated list of actors */
actors: string;
/** Release year */
year: string;
/** Comma-separated genres */
genres_str: string;
/** Age rating */
age: string;
/** IMDB rating as string */
rating_imdb: string;
/** Kinopoisk rating as string */
rating_kinopoisk: string;
/** Screenshot/poster URL */
screenshot_uri: string;
/** Date added to portal */
added: string;
}
@@ -0,0 +1,35 @@
/**
* Stalker portal VOD details response structure.
* Contains movie metadata from Stalker/Ministra portals.
*/
export interface StalkerVodDetails {
/** Unique identifier for the VOD item */
id: string;
/** Command string used to play the stream (e.g., "/media/file_123.mpg") */
cmd: string;
/** VOD metadata */
info: StalkerVodInfo;
}
export interface StalkerVodInfo {
/** Poster/cover image URL */
movie_image: string;
/** Plot description */
description: string;
/** Formatted/system title (often generic like "video_name_format") */
name: string;
/** Original title (preferred, more descriptive) */
o_name?: string;
/** Comma-separated list of actors */
actors: string;
/** Director name */
director: string;
/** Release date (format varies) */
releasedate: string;
/** Genre(s) */
genre: string;
/** IMDB rating as string */
rating_imdb: string;
/** Kinopoisk rating as string */
rating_kinopoisk: string;
}
@@ -0,0 +1,5 @@
/** Labels of properties in local storage */
export enum STORE_KEY {
Settings = 'settings',
Version = 'version'
}
@@ -0,0 +1,7 @@
/**
* Enum with all the formats that are allowed in the application
*/
export enum StreamFormat {
TsStreamFormat = 'ts',
M3u8StreamFormat = 'm3u8',
}
@@ -0,0 +1,5 @@
export enum Theme {
DarkTheme = 'DARK_THEME',
LightTheme = 'LIGHT_THEME',
SystemTheme = 'SYSTEM_THEME',
}
@@ -0,0 +1,159 @@
import { XtreamVodDetails, getXtreamVodInfo } from './xtream-vod-details.interface';
import { StalkerVodDetails } from './stalker-vod-details.interface';
import {
NormalizedVodMeta,
VodDetailsItem,
XtreamVodItem,
StalkerVodItem,
} from './vod-details-item.interface';
/**
* Normalizes Xtream VOD details into a display-friendly format.
* Handles the various field name variations in Xtream API responses.
*/
export function normalizeXtreamVod(item: XtreamVodDetails): NormalizedVodMeta {
const info = getXtreamVodInfo(item);
const movieData = item?.movie_data;
return {
title: info?.name || movieData?.name || 'Unknown',
description: info?.description || info?.plot,
posterUrl: info?.movie_image || info?.cover_big,
backdropUrl: info?.backdrop_path?.[0],
year: extractYear(info?.releasedate),
genre: info?.genre,
duration: info?.duration || formatDuration(info?.duration_secs),
country: info?.country,
director: info?.director,
actors: info?.actors || info?.cast,
ratingImdb: info?.rating_imdb,
ratingKinopoisk: info?.rating_kinopoisk,
youtubeTrailer: info?.youtube_trailer,
};
}
/**
* Normalizes Stalker VOD details into a display-friendly format.
* Stalker has fewer metadata fields than Xtream.
* Prefers o_name (original title) over name (often formatted/generic).
*/
export function normalizeStalkerVod(item: StalkerVodDetails): NormalizedVodMeta {
const info = item?.info;
return {
title: info?.o_name || info?.name || 'Unknown',
description: info?.description,
posterUrl: info?.movie_image,
backdropUrl: undefined, // Stalker doesn't provide backdrop
year: extractYear(info?.releasedate),
genre: info?.genre,
duration: undefined, // Stalker doesn't provide duration
country: undefined, // Stalker doesn't provide country
director: info?.director,
actors: info?.actors,
ratingImdb: info?.rating_imdb,
ratingKinopoisk: info?.rating_kinopoisk,
youtubeTrailer: undefined, // Stalker doesn't provide trailers
};
}
/**
* Normalizes any VodDetailsItem to display-friendly format.
* Uses discriminated union for type-safe handling.
*/
export function normalizeVodDetails(item: VodDetailsItem): NormalizedVodMeta {
if (item.type === 'xtream') {
return normalizeXtreamVod(item.data);
}
return normalizeStalkerVod(item.data);
}
/**
* Creates an XtreamVodItem from raw data and context.
*/
export function createXtreamVodItem(
data: XtreamVodDetails,
playlistId: string,
vodId?: number
): XtreamVodItem {
return {
type: 'xtream',
data,
playlistId,
vodId: vodId ?? data.movie_data?.stream_id ?? 0,
};
}
/**
* Creates a StalkerVodItem from raw data and context.
*/
export function createStalkerVodItem(
data: StalkerVodDetails,
playlistId: string
): StalkerVodItem {
return {
type: 'stalker',
data,
playlistId,
cmd: data.cmd,
};
}
/**
* Gets the unique identifier for a VOD item.
* Returns vodId for Xtream, id for Stalker.
*/
export function getVodId(item: VodDetailsItem): number | string {
if (item.type === 'xtream') {
return item.vodId;
}
return item.data.id;
}
/**
* Gets the numeric ID for download/favorite tracking.
*/
export function getVodNumericId(item: VodDetailsItem): number {
if (item.type === 'xtream') {
return item.vodId;
}
return Number(item.data.id) || 0;
}
// ============ Helper Functions ============
/**
* Extracts 4-digit year from various date formats.
*/
function extractYear(dateString?: string): string | undefined {
if (!dateString) return undefined;
// Try to extract 4-digit year from beginning
const yearMatch = dateString.match(/^(\d{4})/);
if (yearMatch) {
return yearMatch[1];
}
// Try to parse as date and extract year
const date = new Date(dateString);
if (!isNaN(date.getTime())) {
return date.getFullYear().toString();
}
return dateString.slice(0, 4);
}
/**
* Formats duration from seconds to human-readable string.
*/
function formatDuration(seconds?: number): string | undefined {
if (!seconds || seconds <= 0) return undefined;
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}h ${minutes}m`;
}
return `${minutes}m`;
}
@@ -0,0 +1,96 @@
import { XtreamVodDetails } from './xtream-vod-details.interface';
import { StalkerVodDetails } from './stalker-vod-details.interface';
/**
* Discriminated union for VOD details across portal types.
* Enables type-safe handling without runtime type checks.
*
* @example
* ```typescript
* function handleVod(item: VodDetailsItem) {
* if (item.type === 'xtream') {
* // TypeScript knows item.data is XtreamVodDetails
* console.log(item.data.movie_data.stream_id);
* } else {
* // TypeScript knows item.data is StalkerVodDetails
* console.log(item.data.cmd);
* }
* }
* ```
*/
export type VodDetailsItem = XtreamVodItem | StalkerVodItem;
/**
* Xtream Codes VOD item wrapper with context.
*/
export interface XtreamVodItem {
/** Discriminator for Xtream Codes portals */
readonly type: 'xtream';
/** Full VOD details from Xtream API */
data: XtreamVodDetails;
/** Playlist/portal ID for favorites and downloads */
playlistId: string;
/** Stream ID from Xtream API */
vodId: number;
}
/**
* Stalker portal VOD item wrapper with context.
*/
export interface StalkerVodItem {
/** Discriminator for Stalker portals */
readonly type: 'stalker';
/** Full VOD details from Stalker API */
data: StalkerVodDetails;
/** Playlist/portal ID for favorites and downloads */
playlistId: string;
/** Command string for stream playback */
cmd: string;
}
/**
* Normalized metadata for display in templates.
* Provides a consistent interface regardless of portal type.
*/
export interface NormalizedVodMeta {
/** Movie title */
title: string;
/** Plot description */
description?: string;
/** Poster/cover image URL */
posterUrl?: string;
/** Backdrop image URL (Xtream only) */
backdropUrl?: string;
/** Release year (4 digits) */
year?: string;
/** Genre(s) */
genre?: string;
/** Duration string (e.g., "1h 30m") */
duration?: string;
/** Country of origin (Xtream only) */
country?: string;
/** Director name */
director?: string;
/** Actors/cast (comma-separated) */
actors?: string;
/** IMDB rating */
ratingImdb?: string;
/** Kinopoisk rating */
ratingKinopoisk?: string;
/** YouTube trailer ID (Xtream only) */
youtubeTrailer?: string;
}
/**
* Type guard to check if item is Xtream VOD.
*/
export function isXtreamVod(item: VodDetailsItem): item is XtreamVodItem {
return item.type === 'xtream';
}
/**
* Type guard to check if item is Stalker VOD.
*/
export function isStalkerVod(item: VodDetailsItem): item is StalkerVodItem {
return item.type === 'stalker';
}
@@ -0,0 +1,17 @@
import type { Language } from './language.enum';
import type { StreamFormat } from './stream-format.enum';
import type { VideoPlayer } from './settings.interface';
/**
* Optional product branding (e.g. KiraTV). When null, the app behaves as generic IPTVnator.
*/
export interface WorkspaceProductBranding {
readonly appDisplayName: string;
readonly xtreamOnlyUi: boolean;
readonly xtreamDefaultServerUrl: string;
readonly defaultPlaylistTitle: string;
readonly defaultLanguage: Language;
readonly defaultVideoPlayer: VideoPlayer;
readonly defaultStreamFormat: StreamFormat;
readonly indexHtmlLang: string;
}
@@ -0,0 +1,15 @@
export interface XtreamAccountInfoDialogPlaylist {
id: string;
name?: string;
title?: string;
serverUrl: string;
username: string;
password: string;
}
export interface XtreamAccountInfoDialogData {
vodStreamsCount?: number;
liveStreamsCount?: number;
seriesCount?: number;
playlist?: XtreamAccountInfoDialogPlaylist;
}
@@ -0,0 +1,7 @@
export interface XtreamCategory {
id?: number;
category_id: string;
category_name: string;
parent_id: number;
count?: number;
}
@@ -0,0 +1,14 @@
export enum XtreamCodeActions {
GetAccountInfo = 'get_account_info',
GetLiveStreams = 'get_live_streams',
GetLiveCategories = 'get_live_categories',
GetSeries = 'get_series',
GetSeriesCategories = 'get_series_categories',
GetSeriesInfo = 'get_series_info',
GetVodStreams = 'get_vod_streams',
GetVodCategories = 'get_vod_categories',
GetVodInfo = 'get_vod_info',
GetShortEpg = 'get_short_epg',
GetSimpleDataTable = 'get_simple_data_table',
GetSimpleDateTable = 'get_simple_date_table',
}
@@ -0,0 +1,18 @@
export interface XtreamItem {
num: number;
name: string;
stream_type: 'live' | 'movie';
stream_id: number;
stream_icon: string;
added: string;
category_id: string;
custom_sid: string;
direct_source: string;
epg_channel_id?: string;
tv_archive?: number;
tv_archive_duration?: number;
rating_imdb?: string;
xtream_id?: number;
type?: 'movie' | 'series' | 'live';
added_at?: number;
}
@@ -0,0 +1,8 @@
import { XtreamItem } from './xtream-item.interface';
export interface XtreamLiveStream extends XtreamItem {
stream_type: 'live';
epg_channel_id?: string;
tv_archive: number;
tv_archive_duration: number;
}
@@ -0,0 +1,6 @@
import { XtreamCodeActions } from './xtream-code-actions';
export interface XtreamResponse {
payload: unknown;
action: XtreamCodeActions;
}
@@ -0,0 +1,17 @@
import {
XtreamBackupFavoriteItem,
XtreamBackupHiddenCategory,
XtreamBackupRecentlyViewedItem,
} from './playlist-backup.interface';
import { PlaybackPositionData } from './playback-position.interface';
export interface XtreamPendingRestoreState {
hiddenCategories: XtreamBackupHiddenCategory[];
favorites: XtreamBackupFavoriteItem[];
recentlyViewed: XtreamBackupRecentlyViewedItem[];
playbackPositions: PlaybackPositionData[];
}
export function getXtreamPendingRestoreStorageKey(playlistId: string): string {
return `xtream-restore-${playlistId}`;
}
@@ -0,0 +1,59 @@
export interface XtreamSerieDetails {
seasons: XtreamSerieSeason[];
info: XtreamSerieInfo;
episodes: Record<string, XtreamSerieEpisode[]>;
}
export interface XtreamSerieInfo {
name: string;
cover: string;
plot: string;
cast: string;
director: string;
genre: string;
releaseDate: string;
last_modified: string;
rating: string;
rating_5based: number;
backdrop_path: string[];
youtube_trailer: string;
episode_run_time: string;
category_id: string;
}
export interface XtreamSerieEpisode {
id: string;
episode_num: number;
title: string;
container_extension: string;
info: XtreamSerieEpisodeInfo | []; // Can be empty array when no metadata available
custom_sid: string;
added: string;
season: number;
direct_source: string;
}
export interface XtreamSerieEpisodeInfo {
tmdb_id?: number;
releasedate?: string;
plot?: string;
duration_secs?: number;
duration?: string;
movie_image?: string;
video?: Record<string, string>; // TODO
audio?: Record<string, string>; // TODO
bitrate?: number;
rating?: number;
season?: string;
}
export interface XtreamSerieSeason {
air_date: string;
episode_count: number;
id: number;
name: string;
overview: string;
season_number: number;
cover: string;
cover_big: string;
}
@@ -0,0 +1,18 @@
export interface XtreamSerieItem {
num: number;
name: string;
series_id: number;
cover: string;
plot: string;
cast: string;
director: string;
genre: string;
releaseDate: string;
last_modified: string;
rating: string;
rating_5based: number;
backdrop_path: string[];
youtube_trailer: string;
episode_run_time: string;
category_id: number;
}
@@ -0,0 +1,64 @@
export interface XtreamVodInfo {
kinopoisk_url: string;
tmdb_id: number | string;
name: string;
o_name: string;
cover_big: string;
movie_image: string;
releasedate: string;
episode_run_time: number;
youtube_trailer: string;
director: string;
actors: string;
cast: string;
description: string;
plot: string;
age: string;
mpaa_rating: string;
rating_count_kinopoisk: number;
country: string;
genre: string;
backdrop_path: string[];
duration_secs: number;
duration: string;
video: string[];
audio: string[];
bitrate: number;
rating: number | string;
runtime?: string;
status?: string;
rating_kinopoisk?: string;
rating_imdb?: string;
}
export interface XtreamVodMovieData {
stream_id: number;
name: string;
added: string;
category_id: string;
container_extension: string;
custom_sid: string | null;
direct_source: string;
category_ids?: number[];
}
export interface XtreamVodDetails {
info?: XtreamVodInfo | [] | null;
movie_data?: XtreamVodMovieData;
}
export function getXtreamVodInfo(
details: Pick<XtreamVodDetails, 'info'> | null | undefined
): XtreamVodInfo | null {
const info = details?.info;
if (!info || Array.isArray(info) || typeof info !== 'object') {
return null;
}
return info;
}
export function hasXtreamVodInfo(
details: Pick<XtreamVodDetails, 'info'> | null | undefined
): details is XtreamVodDetails & { info: XtreamVodInfo } {
return getXtreamVodInfo(details) !== null;
}
@@ -0,0 +1,8 @@
import { XtreamItem } from './xtream-item.interface';
export interface XtreamVodStream extends XtreamItem {
stream_type: 'movie';
rating: number;
rating_5based: number;
container_extension: string;
}
+23
View File
@@ -0,0 +1,23 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"forceConsistentCasingInFileNames": true,
"strict": true,
"importHelpers": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noPropertyAccessFromIndexSignature": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts", "../../../global.d.ts"],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
}
+15
View File
@@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}