Proyecto App Linux
This commit is contained in:
@@ -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'),
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user