mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
refactor(electron): share typed preload bridge contract (#1018)
* refactor(electron): share typed preload bridge contract * refactor(electron): tighten bridge review fixes * refactor(electron): tighten playlist bridge returns * refactor(electron): use bridge epg progress type * refactor(epg): alias progress bridge types
This commit is contained in:
1 parent
4847eb5241
commit
1badb9a057
14 files changed
+1080
-1256
No files matched your search
@@ -543,6 +543,7 @@ This project uses modern Angular signal-based APIs and patterns. **ALWAYS** use
|
||||
- **Preload script**: `apps/electron-backend/src/app/api/main.preload.ts`
|
||||
- Exposes `window.electron` API via `contextBridge`
|
||||
- All IPC channels defined here (playlist operations, EPG, database CRUD, external players, etc.)
|
||||
- The canonical TypeScript contract is `ElectronBridgeApi` in `libs/shared/interfaces/src/lib/electron-api.interface.ts`; `global.d.ts`, `apps/web/src/typings.d.ts`, and `main.preload.ts` must reference this shared type instead of maintaining separate method lists.
|
||||
- **Event handlers**: `apps/electron-backend/src/app/events/`
|
||||
- `database.events.ts` - Database CRUD operations
|
||||
- `playlist.events.ts` - Playlist import/update
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import type { ElectronBridgeApi } from '@iptvnator/shared/interfaces';
|
||||
|
||||
type ElectronBridgeMethodName = Extract<
|
||||
{
|
||||
[K in keyof ElectronBridgeApi]: ElectronBridgeApi[K] extends (
|
||||
...args: never[]
|
||||
) => unknown
|
||||
? K
|
||||
: never;
|
||||
}[keyof ElectronBridgeApi],
|
||||
string
|
||||
>;
|
||||
|
||||
type PreloadInvokeCase = {
|
||||
method: string;
|
||||
method: ElectronBridgeMethodName;
|
||||
args: unknown[];
|
||||
channel: string;
|
||||
forwardedArgs: unknown[];
|
||||
@@ -7,6 +20,8 @@ type PreloadInvokeCase = {
|
||||
|
||||
const playlistId = 'playlist-1';
|
||||
export const operationId = 'operation-1';
|
||||
const epgUrls = ['https://example.com/guide.xml'];
|
||||
const channelIds = ['channel-1', 'channel-2'];
|
||||
const playlist = { id: playlistId, name: 'Playlist', type: 'xtream' };
|
||||
const playlists = [playlist];
|
||||
const playlistUpdates = { name: 'Updated playlist' };
|
||||
@@ -319,3 +334,66 @@ export const dbPreloadCases: PreloadInvokeCase[] = [
|
||||
forwardedArgs: [playlistId, 42, 'vod'],
|
||||
},
|
||||
];
|
||||
|
||||
export const epgPreloadCases: PreloadInvokeCase[] = [
|
||||
{
|
||||
method: 'fetchEpg',
|
||||
args: [epgUrls],
|
||||
channel: 'FETCH_EPG',
|
||||
forwardedArgs: [{ url: epgUrls }],
|
||||
},
|
||||
{
|
||||
method: 'getChannelPrograms',
|
||||
args: ['channel-1'],
|
||||
channel: 'GET_CHANNEL_PROGRAMS',
|
||||
forwardedArgs: [{ channelId: 'channel-1' }],
|
||||
},
|
||||
{
|
||||
method: 'getCurrentProgramsBatch',
|
||||
args: [channelIds],
|
||||
channel: 'GET_CURRENT_PROGRAMS_BATCH',
|
||||
forwardedArgs: [{ channelIds }],
|
||||
},
|
||||
{
|
||||
method: 'getEpgChannelMetadata',
|
||||
args: [channelIds],
|
||||
channel: 'EPG_GET_CHANNEL_METADATA',
|
||||
forwardedArgs: [{ channelIds }],
|
||||
},
|
||||
{
|
||||
method: 'getEpgChannels',
|
||||
args: [],
|
||||
channel: 'EPG_GET_CHANNELS',
|
||||
forwardedArgs: [],
|
||||
},
|
||||
{
|
||||
method: 'getEpgChannelsByRange',
|
||||
args: [10, 20],
|
||||
channel: 'EPG_GET_CHANNELS_BY_RANGE',
|
||||
forwardedArgs: [{ skip: 10, limit: 20 }],
|
||||
},
|
||||
{
|
||||
method: 'forceFetchEpg',
|
||||
args: ['https://example.com/guide.xml'],
|
||||
channel: 'EPG_FORCE_FETCH',
|
||||
forwardedArgs: ['https://example.com/guide.xml'],
|
||||
},
|
||||
{
|
||||
method: 'clearEpgData',
|
||||
args: [],
|
||||
channel: 'EPG_CLEAR_ALL',
|
||||
forwardedArgs: [],
|
||||
},
|
||||
{
|
||||
method: 'checkEpgFreshness',
|
||||
args: [epgUrls, 24],
|
||||
channel: 'EPG_CHECK_FRESHNESS',
|
||||
forwardedArgs: [{ urls: epgUrls, maxAgeHours: 24 }],
|
||||
},
|
||||
{
|
||||
method: 'searchEpgPrograms',
|
||||
args: ['news', 50],
|
||||
channel: 'EPG_DB_SEARCH_PROGRAMS',
|
||||
forwardedArgs: ['news', 50],
|
||||
},
|
||||
];
|
||||
@@ -2,9 +2,15 @@ import {
|
||||
DB_WORKER_OPERATIONS,
|
||||
DbWorkerOperation,
|
||||
} from '../workers/database-worker.types';
|
||||
import { dbPreloadCases, operationId } from './main.preload.spec-data';
|
||||
import type { ElectronBridgeApi } from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
dbPreloadCases,
|
||||
epgPreloadCases,
|
||||
operationId,
|
||||
} from './main.preload.spec-data';
|
||||
|
||||
type ExposedElectronApi = Record<string, (...args: unknown[]) => unknown>;
|
||||
type ExposedElectronApi = ElectronBridgeApi &
|
||||
Record<string, (...args: unknown[]) => unknown>;
|
||||
|
||||
type MockIpcRenderer = {
|
||||
invoke: jest.Mock;
|
||||
@@ -25,6 +31,15 @@ function getExposedApi(): ExposedElectronApi {
|
||||
return mockExposedApi;
|
||||
}
|
||||
|
||||
function callExposedApiMethod(
|
||||
api: ExposedElectronApi,
|
||||
method: string,
|
||||
args: unknown[]
|
||||
): unknown {
|
||||
const callableApi = api as Record<string, (...args: unknown[]) => unknown>;
|
||||
return callableApi[method](...args);
|
||||
}
|
||||
|
||||
describe('main preload DB IPC contract', () => {
|
||||
beforeEach(async () => {
|
||||
jest.resetModules();
|
||||
@@ -75,7 +90,21 @@ describe('main preload DB IPC contract', () => {
|
||||
async ({ method, args, channel, forwardedArgs }) => {
|
||||
const api = getExposedApi();
|
||||
|
||||
await api[method](...args);
|
||||
await callExposedApiMethod(api, method, args);
|
||||
|
||||
expect(mockIpcRenderer.invoke).toHaveBeenLastCalledWith(
|
||||
channel,
|
||||
...forwardedArgs
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it.each(epgPreloadCases)(
|
||||
'$method invokes $channel with the expected arguments',
|
||||
async ({ method, args, channel, forwardedArgs }) => {
|
||||
const api = getExposedApi();
|
||||
|
||||
await callExposedApiMethod(api, method, args);
|
||||
|
||||
expect(mockIpcRenderer.invoke).toHaveBeenLastCalledWith(
|
||||
channel,
|
||||
|
||||
@@ -4,10 +4,25 @@ import type {
|
||||
EmbeddedMpvRecordingStartOptions,
|
||||
EmbeddedMpvSession,
|
||||
EmbeddedMpvSupport,
|
||||
ElectronBridgeApi,
|
||||
ElectronBridgeDbOperationEvent,
|
||||
ElectronBridgeDownloadStartPayload,
|
||||
ElectronBridgeEpgProgress,
|
||||
ElectronBridgePlaybackPositionInput,
|
||||
ElectronBridgePlaylistInput,
|
||||
ElectronBridgePlaylistUpsertInput,
|
||||
ElectronBridgeRemoteControlCommand,
|
||||
ElectronBridgeRemoteControlStatus,
|
||||
ElectronBridgeXtreamContentStream,
|
||||
ExternalPlayerSession,
|
||||
PlaybackPositionData,
|
||||
PlayerContentInfo,
|
||||
Playlist,
|
||||
PlaylistRefreshEvent,
|
||||
PlaylistRefreshPayload,
|
||||
PortalDebugEvent,
|
||||
ResolvedPortalPlayback,
|
||||
Settings,
|
||||
XtreamCategory,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
@@ -23,33 +38,11 @@ const EMBEDDED_MPV_SESSION_UPDATE = 'EMBEDDED_MPV_SESSION_UPDATE';
|
||||
const DB_OPERATION_EVENT = 'DB_OPERATION_EVENT';
|
||||
const PLAYLIST_REFRESH_EVENT = 'PLAYLIST:REFRESH_EVENT';
|
||||
|
||||
type PortalDebugEvent = {
|
||||
requestId: string;
|
||||
provider: 'xtream' | 'stalker';
|
||||
operation: string;
|
||||
transport: 'electron-main' | 'electron-renderer' | 'pwa-http';
|
||||
startedAt: string;
|
||||
durationMs: number;
|
||||
status: 'success' | 'error';
|
||||
request: unknown;
|
||||
response?: unknown;
|
||||
error?: unknown;
|
||||
};
|
||||
|
||||
type DbOperationEvent = {
|
||||
operationId?: string;
|
||||
operation: string;
|
||||
playlistId?: string;
|
||||
status: 'started' | 'progress' | 'completed' | 'cancelled' | 'error';
|
||||
phase?: string;
|
||||
current?: number;
|
||||
total?: number;
|
||||
increment?: number;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
const dbSaveContentProgressListeners = new Set<
|
||||
(event: Electron.IpcRendererEvent, data: DbOperationEvent) => void
|
||||
(
|
||||
event: Electron.IpcRendererEvent,
|
||||
data: ElectronBridgeDbOperationEvent
|
||||
) => void
|
||||
>();
|
||||
|
||||
const shouldTraceRendererApi = isRendererApiTraceEnabled();
|
||||
@@ -69,7 +62,7 @@ function emitRendererTrace(payload: {
|
||||
ipcRenderer.send(DEBUG_TRACE_EVENT_CHANNEL, payload);
|
||||
}
|
||||
|
||||
function wrapElectronApi<T extends Record<string, unknown>>(api: T): T {
|
||||
function wrapElectronApi<T extends object>(api: T): T {
|
||||
if (!shouldTraceRendererApi) {
|
||||
return api;
|
||||
}
|
||||
@@ -163,32 +156,29 @@ function wrapElectronApi<T extends Record<string, unknown>>(api: T): T {
|
||||
) as T;
|
||||
}
|
||||
|
||||
const electronApi = {
|
||||
const electronApi: ElectronBridgeApi = {
|
||||
// Remote control channel change listener
|
||||
onChannelChange: (
|
||||
callback: (data: { direction: 'up' | 'down' }) => void
|
||||
) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, data: any) =>
|
||||
callback(data);
|
||||
const handler = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
data: { direction: 'up' | 'down' }
|
||||
) => callback(data);
|
||||
ipcRenderer.on('CHANNEL_CHANGE', handler);
|
||||
return () => ipcRenderer.off('CHANNEL_CHANGE', handler);
|
||||
},
|
||||
onRemoteControlCommand: (
|
||||
callback: (data: {
|
||||
type:
|
||||
| 'channel-select-number'
|
||||
| 'volume-up'
|
||||
| 'volume-down'
|
||||
| 'volume-toggle-mute';
|
||||
number?: number;
|
||||
}) => void
|
||||
callback: (data: ElectronBridgeRemoteControlCommand) => void
|
||||
) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, data: any) =>
|
||||
callback(data);
|
||||
const handler = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
data: ElectronBridgeRemoteControlCommand
|
||||
) => callback(data);
|
||||
ipcRenderer.on('REMOTE_CONTROL_COMMAND', handler);
|
||||
return () => ipcRenderer.off('REMOTE_CONTROL_COMMAND', handler);
|
||||
},
|
||||
updateRemoteControlStatus: (status: any) => {
|
||||
updateRemoteControlStatus: (status: ElectronBridgeRemoteControlStatus) => {
|
||||
ipcRenderer.send('REMOTE_CONTROL_STATUS_UPDATE', status);
|
||||
},
|
||||
// Player error listener
|
||||
@@ -202,26 +192,25 @@ const electronApi = {
|
||||
ipcRenderer.on('player-error', (_event, data) => callback(data));
|
||||
},
|
||||
onPortalDebugEvent: (callback: (data: PortalDebugEvent) => void) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, data: any) =>
|
||||
callback(data as PortalDebugEvent);
|
||||
const handler = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
data: PortalDebugEvent
|
||||
) => callback(data);
|
||||
ipcRenderer.on(PORTAL_DEBUG_EVENT, handler);
|
||||
return () => ipcRenderer.off(PORTAL_DEBUG_EVENT, handler);
|
||||
},
|
||||
// EPG progress listener
|
||||
onEpgProgress: (
|
||||
callback: (data: {
|
||||
url: string;
|
||||
status: 'loading' | 'complete' | 'error';
|
||||
stats?: { totalChannels: number; totalPrograms: number };
|
||||
error?: string;
|
||||
}) => void
|
||||
) => {
|
||||
onEpgProgress: (callback: (data: ElectronBridgeEpgProgress) => void) => {
|
||||
ipcRenderer.on('EPG_PROGRESS_UPDATE', (_event, data) => callback(data));
|
||||
},
|
||||
// Playback position update listener - returns unsubscribe function
|
||||
onPlaybackPositionUpdate: (callback: (data: any) => void) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, data: any) =>
|
||||
callback(data);
|
||||
onPlaybackPositionUpdate: (
|
||||
callback: (data: PlaybackPositionData) => void
|
||||
) => {
|
||||
const handler = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
data: PlaybackPositionData
|
||||
) => callback(data);
|
||||
ipcRenderer.on('playback-position-update', handler);
|
||||
return () => ipcRenderer.off('playback-position-update', handler);
|
||||
},
|
||||
@@ -245,17 +234,23 @@ const electronApi = {
|
||||
ipcRenderer.on(EMBEDDED_MPV_SESSION_UPDATE, handler);
|
||||
return () => ipcRenderer.off(EMBEDDED_MPV_SESSION_UPDATE, handler);
|
||||
},
|
||||
onDbOperationEvent: (callback: (data: DbOperationEvent) => void) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, data: any) =>
|
||||
callback(data as DbOperationEvent);
|
||||
onDbOperationEvent: (
|
||||
callback: (data: ElectronBridgeDbOperationEvent) => void
|
||||
) => {
|
||||
const handler = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
data: ElectronBridgeDbOperationEvent
|
||||
) => callback(data);
|
||||
ipcRenderer.on(DB_OPERATION_EVENT, handler);
|
||||
return () => ipcRenderer.off(DB_OPERATION_EVENT, handler);
|
||||
},
|
||||
onPlaylistRefreshEvent: (
|
||||
callback: (data: PlaylistRefreshEvent) => void
|
||||
) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, data: any) =>
|
||||
callback(data as PlaylistRefreshEvent);
|
||||
const handler = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
data: PlaylistRefreshEvent
|
||||
) => callback(data);
|
||||
ipcRenderer.on(PLAYLIST_REFRESH_EVENT, handler);
|
||||
return () => ipcRenderer.off(PLAYLIST_REFRESH_EVENT, handler);
|
||||
},
|
||||
@@ -263,7 +258,7 @@ const electronApi = {
|
||||
onDbSaveContentProgress: (callback: (count: number) => void) => {
|
||||
const handler = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
data: DbOperationEvent
|
||||
data: ElectronBridgeDbOperationEvent
|
||||
) => {
|
||||
if (
|
||||
data.operation !== 'save-content' ||
|
||||
@@ -311,7 +306,7 @@ const electronApi = {
|
||||
userAgent: string | undefined,
|
||||
referer?: string,
|
||||
origin?: string,
|
||||
contentInfo?: any,
|
||||
contentInfo?: PlayerContentInfo,
|
||||
startTime?: number,
|
||||
headers?: Record<string, string>
|
||||
): Promise<ExternalPlayerSession> =>
|
||||
@@ -334,7 +329,7 @@ const electronApi = {
|
||||
userAgent: string | undefined,
|
||||
referer?: string,
|
||||
origin?: string,
|
||||
contentInfo?: any,
|
||||
contentInfo?: PlayerContentInfo,
|
||||
startTime?: number,
|
||||
headers?: Record<string, string>
|
||||
): Promise<ExternalPlayerSession> =>
|
||||
@@ -456,7 +451,7 @@ const electronApi = {
|
||||
ipcRenderer.invoke('SET_MPV_PLAYER_PATH', mpvPlayerPath),
|
||||
setVlcPlayerPath: (vlcPlayerPath: string) =>
|
||||
ipcRenderer.invoke('SET_VLC_PLAYER_PATH', vlcPlayerPath),
|
||||
updateSettings: (settings: any) =>
|
||||
updateSettings: (settings: Partial<Settings>) =>
|
||||
ipcRenderer.invoke('SETTINGS_UPDATE', settings),
|
||||
getAiSettings: () => ipcRenderer.invoke('GET_AI_SETTINGS'),
|
||||
stalkerRequest: (payload: {
|
||||
@@ -483,19 +478,21 @@ const electronApi = {
|
||||
cancelPlaylistRefresh: (operationId: string) =>
|
||||
ipcRenderer.invoke('PLAYLIST:CANCEL_REFRESH', operationId),
|
||||
// Database operations
|
||||
dbCreatePlaylist: (playlist: any) =>
|
||||
dbCreatePlaylist: (playlist: ElectronBridgePlaylistUpsertInput) =>
|
||||
ipcRenderer.invoke('DB_CREATE_PLAYLIST', playlist),
|
||||
dbGetPlaylist: (playlistId: string) =>
|
||||
ipcRenderer.invoke('DB_GET_PLAYLIST', playlistId),
|
||||
dbUpsertAppPlaylist: (playlist: any) =>
|
||||
dbUpsertAppPlaylist: (playlist: Playlist) =>
|
||||
ipcRenderer.invoke('DB_UPSERT_APP_PLAYLIST', playlist),
|
||||
dbUpsertAppPlaylists: (playlists: any[]) =>
|
||||
dbUpsertAppPlaylists: (playlists: Playlist[]) =>
|
||||
ipcRenderer.invoke('DB_UPSERT_APP_PLAYLISTS', playlists),
|
||||
dbGetAppPlaylists: () => ipcRenderer.invoke('DB_GET_APP_PLAYLISTS'),
|
||||
dbGetAppPlaylist: (playlistId: string) =>
|
||||
ipcRenderer.invoke('DB_GET_APP_PLAYLIST', playlistId),
|
||||
dbUpdatePlaylist: (playlistId: string, updates: any) =>
|
||||
ipcRenderer.invoke('DB_UPDATE_PLAYLIST', playlistId, updates),
|
||||
dbUpdatePlaylist: (
|
||||
playlistId: string,
|
||||
updates: Partial<Playlist> | ElectronBridgePlaylistInput
|
||||
) => ipcRenderer.invoke('DB_UPDATE_PLAYLIST', playlistId, updates),
|
||||
dbDeletePlaylist: (playlistId: string, operationId?: string) =>
|
||||
ipcRenderer.invoke('DB_DELETE_PLAYLIST', playlistId, operationId),
|
||||
dbDeleteXtreamContent: (playlistId: string, operationId?: string) =>
|
||||
@@ -544,7 +541,7 @@ const electronApi = {
|
||||
ipcRenderer.invoke('DB_GET_CONTENT', playlistId, type),
|
||||
dbSaveContent: (
|
||||
playlistId: string,
|
||||
streams: any[],
|
||||
streams: ElectronBridgeXtreamContentStream[],
|
||||
type: string,
|
||||
operationId?: string
|
||||
) =>
|
||||
@@ -671,8 +668,10 @@ const electronApi = {
|
||||
dbSetAppState: (key: string, value: string) =>
|
||||
ipcRenderer.invoke('DB_SET_APP_STATE', key, value),
|
||||
// Playback Positions
|
||||
dbSavePlaybackPosition: (playlistId: string, data: any) =>
|
||||
ipcRenderer.invoke('DB_SAVE_PLAYBACK_POSITION', playlistId, data),
|
||||
dbSavePlaybackPosition: (
|
||||
playlistId: string,
|
||||
data: ElectronBridgePlaybackPositionInput
|
||||
) => ipcRenderer.invoke('DB_SAVE_PLAYBACK_POSITION', playlistId, data),
|
||||
dbGetPlaybackPosition: (
|
||||
playlistId: string,
|
||||
contentXtreamId: number,
|
||||
@@ -716,30 +715,8 @@ const electronApi = {
|
||||
),
|
||||
getLocalIpAddresses: () => ipcRenderer.invoke('get-local-ip-addresses'),
|
||||
// Downloads
|
||||
downloadsStart: (data: {
|
||||
playlistId: string;
|
||||
xtreamId: number;
|
||||
contentType: 'vod' | 'episode';
|
||||
title: string;
|
||||
url: string;
|
||||
posterUrl?: string;
|
||||
downloadFolder: string;
|
||||
headers?: { userAgent?: string; referer?: string; origin?: string };
|
||||
seriesXtreamId?: number;
|
||||
seasonNumber?: number;
|
||||
episodeNumber?: number;
|
||||
// Playlist info for auto-creation if needed
|
||||
playlistName?: string;
|
||||
playlistType?:
|
||||
| 'xtream'
|
||||
| 'stalker'
|
||||
| 'm3u-file'
|
||||
| 'm3u-text'
|
||||
| 'm3u-url';
|
||||
serverUrl?: string;
|
||||
portalUrl?: string;
|
||||
macAddress?: string;
|
||||
}) => ipcRenderer.invoke('DOWNLOADS_START', data),
|
||||
downloadsStart: (data: ElectronBridgeDownloadStartPayload) =>
|
||||
ipcRenderer.invoke('DOWNLOADS_START', data),
|
||||
downloadsCancel: (downloadId: number) =>
|
||||
ipcRenderer.invoke('DOWNLOADS_CANCEL', downloadId),
|
||||
downloadsRetry: (downloadId: number, downloadFolder: string) =>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
AUTO_UPDATE_PLAYLISTS,
|
||||
createDevLogger,
|
||||
ERROR,
|
||||
PlayerContentInfo,
|
||||
Playlist,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
PLAYLIST_UPDATE,
|
||||
@@ -30,7 +31,7 @@ interface PlayerLaunchPayload {
|
||||
readonly title?: string;
|
||||
readonly url: string;
|
||||
readonly ['user-agent']?: string;
|
||||
readonly contentInfo?: unknown;
|
||||
readonly contentInfo?: PlayerContentInfo;
|
||||
}
|
||||
|
||||
interface ErrorStatus {
|
||||
|
||||
Vendored
+2
-537
@@ -4,547 +4,12 @@ interface NodeModule {
|
||||
id: string;
|
||||
}
|
||||
|
||||
import {
|
||||
EmbeddedMpvBounds,
|
||||
EmbeddedMpvRecordingStartOptions,
|
||||
EmbeddedMpvSession,
|
||||
EmbeddedMpvSupport,
|
||||
EpgChannel,
|
||||
EpgChannelMetadata,
|
||||
EpgProgram,
|
||||
ExternalPlayerSession,
|
||||
PlaybackPositionData,
|
||||
Playlist,
|
||||
PlaylistRefreshEvent,
|
||||
PlaylistRefreshPayload,
|
||||
PortalDebugEvent,
|
||||
ResolvedPortalPlayback,
|
||||
Settings,
|
||||
XtreamBackupFavoriteItem,
|
||||
XtreamBackupHiddenCategory,
|
||||
XtreamBackupRecentlyViewedItem,
|
||||
XtreamCategory,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
DbOperationEvent,
|
||||
GlobalFavoriteItem,
|
||||
GlobalRecentlyAddedItem,
|
||||
GlobalRecentItem,
|
||||
GlobalSearchResult,
|
||||
XCategoryFromDb,
|
||||
XtreamContent,
|
||||
} from '@iptvnator/services';
|
||||
|
||||
type JsonObject = Record<string, unknown>;
|
||||
type JsonArray = unknown[];
|
||||
import type { ElectronBridgeApi } from '@iptvnator/shared/interfaces';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: {
|
||||
onPortalDebugEvent?: (
|
||||
callback: (data: PortalDebugEvent) => void
|
||||
) => () => void;
|
||||
onPlaylistRefreshEvent?: (
|
||||
callback: (data: PlaylistRefreshEvent) => void
|
||||
) => () => void;
|
||||
getAppVersion: () => Promise<string>;
|
||||
platform: string;
|
||||
fetchPlaylistByUrl: (
|
||||
url: string,
|
||||
title?: string
|
||||
) => Promise<Playlist>;
|
||||
updatePlaylistFromFilePath: (
|
||||
filePath: string,
|
||||
title: string
|
||||
) => Promise<Playlist>;
|
||||
openPlaylistFromFile: () => Promise<Playlist | null>;
|
||||
getPathForFile: (file: File) => string;
|
||||
saveFileDialog: (
|
||||
defaultPath: string,
|
||||
filters?: { name: string; extensions: string[] }[]
|
||||
) => Promise<string | null>;
|
||||
writeFile: (
|
||||
filePath: string,
|
||||
content: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
setUserAgent: (
|
||||
userAgent?: string | null,
|
||||
referer?: string | null,
|
||||
scopeUrl?: string | null
|
||||
) => Promise<boolean>;
|
||||
openInMpv: (
|
||||
url: string,
|
||||
title: string,
|
||||
thumbnail: string,
|
||||
userAgent: string | undefined,
|
||||
referer?: string,
|
||||
origin?: string,
|
||||
contentInfo?: unknown,
|
||||
startTime?: number,
|
||||
headers?: Record<string, string>
|
||||
) => Promise<ExternalPlayerSession>;
|
||||
openInVlc: (
|
||||
url: string,
|
||||
title: string,
|
||||
thumbnail: string,
|
||||
userAgent: string | undefined,
|
||||
referer?: string,
|
||||
origin?: string,
|
||||
contentInfo?: unknown,
|
||||
startTime?: number,
|
||||
headers?: Record<string, string>
|
||||
) => Promise<ExternalPlayerSession>;
|
||||
autoUpdatePlaylists: (playlists: Playlist[]) => Promise<Playlist[]>;
|
||||
fetchEpg: (urls: string[]) => Promise<{
|
||||
success: boolean;
|
||||
message?: string;
|
||||
skipped?: string[];
|
||||
}>;
|
||||
getChannelPrograms: (channelId: string) => Promise<EpgProgram[]>;
|
||||
getCurrentProgramsBatch: (
|
||||
channelIds: string[]
|
||||
) => Promise<Record<string, EpgProgram | null>>;
|
||||
getEpgChannelMetadata: (
|
||||
channelIds: string[]
|
||||
) => Promise<Record<string, EpgChannelMetadata | null>>;
|
||||
getEpgChannels: () => Promise<EpgChannel[]>;
|
||||
getEpgChannelsByRange: (
|
||||
skip: number,
|
||||
limit: number
|
||||
) => Promise<EpgChannel[]>;
|
||||
forceFetchEpg: (
|
||||
url: string
|
||||
) => Promise<{ success: boolean; message?: string }>;
|
||||
clearEpgData: () => Promise<{ success: boolean }>;
|
||||
checkEpgFreshness: (
|
||||
urls: string[],
|
||||
maxAgeHours?: number
|
||||
) => Promise<{ staleUrls: string[]; freshUrls: string[] }>;
|
||||
searchEpgPrograms: (
|
||||
searchTerm: string,
|
||||
limit?: number
|
||||
) => Promise<EpgProgram[]>;
|
||||
updateSettings: (settings: Partial<Settings>) => Promise<void>;
|
||||
getAiSettings: () => Promise<{
|
||||
aiProvider: string;
|
||||
aiModelName: string;
|
||||
aiApiKey: string;
|
||||
}>;
|
||||
setMpvPlayerPath: (mpvPlayerPath: string) => Promise<void>;
|
||||
setVlcPlayerPath: (vlcPlayerPath: string) => Promise<void>;
|
||||
stalkerRequest: (payload: {
|
||||
url: string;
|
||||
macAddress: string;
|
||||
params: Record<string, string>;
|
||||
token?: string;
|
||||
serialNumber?: string;
|
||||
requestId?: string;
|
||||
}) => Promise<JsonObject>;
|
||||
xtreamRequest: (payload: {
|
||||
url: string;
|
||||
params: Record<string, string>;
|
||||
requestId?: string;
|
||||
sessionId?: string;
|
||||
suppressErrorLog?: boolean;
|
||||
}) => Promise<{ payload: unknown; action: string }>;
|
||||
xtreamCancelSession: (
|
||||
sessionId: string
|
||||
) => Promise<{ success: boolean; cancelled: number }>;
|
||||
xtreamProbeUrl: (
|
||||
url: string,
|
||||
method?: 'GET' | 'HEAD'
|
||||
) => Promise<{ status: number; url: string }>;
|
||||
refreshPlaylist: (
|
||||
payload: PlaylistRefreshPayload
|
||||
) => Promise<Playlist>;
|
||||
cancelPlaylistRefresh: (
|
||||
operationId: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
// Database operations
|
||||
dbCreatePlaylist: (
|
||||
playlist: Playlist
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbGetPlaylist: (playlistId: string) => Promise<Playlist | null>;
|
||||
dbUpsertAppPlaylist: (
|
||||
playlist: Playlist
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbUpsertAppPlaylists: (
|
||||
playlists: Playlist[]
|
||||
) => Promise<{ success: boolean; count: number }>;
|
||||
dbGetAppPlaylists: () => Promise<Playlist[]>;
|
||||
dbGetAppPlaylist: (playlistId: string) => Promise<Playlist | null>;
|
||||
dbUpdatePlaylist: (
|
||||
playlistId: string,
|
||||
updates: Partial<Playlist>
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbDeletePlaylist: (
|
||||
playlistId: string,
|
||||
operationId?: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbDeleteXtreamContent: (
|
||||
playlistId: string,
|
||||
operationId?: string
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
favorites: XtreamBackupFavoriteItem[];
|
||||
recentlyViewed: XtreamBackupRecentlyViewedItem[];
|
||||
hiddenCategories: XtreamBackupHiddenCategory[];
|
||||
}>;
|
||||
dbRestoreXtreamUserData: (
|
||||
playlistId: string,
|
||||
favorites: XtreamBackupFavoriteItem[],
|
||||
recentlyViewed: XtreamBackupRecentlyViewedItem[],
|
||||
operationId?: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbHasCategories: (
|
||||
playlistId: string,
|
||||
type: string
|
||||
) => Promise<boolean>;
|
||||
dbGetCategories: (
|
||||
playlistId: string,
|
||||
type: string
|
||||
) => Promise<XCategoryFromDb[]>;
|
||||
dbSaveCategories: (
|
||||
playlistId: string,
|
||||
categories: XtreamCategory[],
|
||||
type: string,
|
||||
hiddenCategoryXtreamIds?: number[]
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbGetAllCategories: (
|
||||
playlistId: string,
|
||||
type: string
|
||||
) => Promise<XCategoryFromDb[]>;
|
||||
dbUpdateCategoryVisibility: (
|
||||
categoryIds: number[],
|
||||
hidden: boolean
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbHasContent: (
|
||||
playlistId: string,
|
||||
type: string
|
||||
) => Promise<boolean>;
|
||||
dbGetContent: (
|
||||
playlistId: string,
|
||||
type: string
|
||||
) => Promise<XtreamContent[]>;
|
||||
dbSaveContent: (
|
||||
playlistId: string,
|
||||
streams: JsonArray,
|
||||
type: string,
|
||||
operationId?: string
|
||||
) => Promise<{ success: boolean; count: number }>;
|
||||
dbClearXtreamImportCache: (
|
||||
playlistId: string,
|
||||
type: 'live' | 'movie' | 'series'
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbSearchContent: (
|
||||
playlistId: string,
|
||||
searchTerm: string,
|
||||
types: string[],
|
||||
excludeHidden?: boolean
|
||||
) => Promise<XtreamContent[]>;
|
||||
dbGlobalSearch: (
|
||||
searchTerm: string,
|
||||
types: string[],
|
||||
excludeHidden?: boolean
|
||||
) => Promise<GlobalSearchResult[]>;
|
||||
dbGetGlobalRecentlyAdded: (
|
||||
kind: 'all' | 'vod' | 'series',
|
||||
limit?: number
|
||||
) => Promise<GlobalRecentlyAddedItem[]>;
|
||||
dbGetRecentlyViewed: () => Promise<GlobalRecentItem[]>;
|
||||
dbClearRecentlyViewed: () => Promise<{ success: boolean }>;
|
||||
// Favorites
|
||||
dbAddFavorite: (
|
||||
contentId: number,
|
||||
playlistId: string,
|
||||
backdropUrl?: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbRemoveFavorite: (
|
||||
contentId: number,
|
||||
playlistId: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbIsFavorite: (
|
||||
contentId: number,
|
||||
playlistId: string
|
||||
) => Promise<boolean>;
|
||||
dbGetFavorites: (playlistId: string) => Promise<XtreamContent[]>;
|
||||
dbGetGlobalFavorites: () => Promise<GlobalFavoriteItem[]>;
|
||||
dbGetAllGlobalFavorites: () => Promise<GlobalFavoriteItem[]>;
|
||||
dbReorderGlobalFavorites: (
|
||||
updates: { content_id: number; position: number }[]
|
||||
) => Promise<{ success: boolean }>;
|
||||
// Recently viewed (playlist-specific)
|
||||
dbGetRecentItems: (playlistId: string) => Promise<XtreamContent[]>;
|
||||
dbAddRecentItem: (
|
||||
contentId: number,
|
||||
playlistId: string,
|
||||
backdropUrl?: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbClearPlaylistRecentItems: (
|
||||
playlistId: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbRemoveRecentItem: (
|
||||
contentId: number,
|
||||
playlistId: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbRemoveRecentItemsBatch: (
|
||||
items: { contentId: number; playlistId: string }[]
|
||||
) => Promise<{ success: boolean; count: number }>;
|
||||
dbGetContentByXtreamId: (
|
||||
xtreamId: number,
|
||||
playlistId: string,
|
||||
contentType?: 'live' | 'movie' | 'series'
|
||||
) => Promise<XtreamContent | null>;
|
||||
dbSetContentBackdropIfMissing: (
|
||||
contentId: number,
|
||||
backdropUrl?: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbGetAppState: (key: string) => Promise<string | null>;
|
||||
dbSetAppState: (
|
||||
key: string,
|
||||
value: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
// Remote control
|
||||
onChannelChange?: (
|
||||
callback: (data: { direction: 'up' | 'down' }) => void
|
||||
) => () => void;
|
||||
onRemoteControlCommand?: (
|
||||
callback: (data: {
|
||||
type:
|
||||
| 'channel-select-number'
|
||||
| 'volume-up'
|
||||
| 'volume-down'
|
||||
| 'volume-toggle-mute';
|
||||
number?: number;
|
||||
}) => void
|
||||
) => () => void;
|
||||
updateRemoteControlStatus?: (status: {
|
||||
portal: 'm3u' | 'xtream' | 'stalker' | 'unknown';
|
||||
isLiveView: boolean;
|
||||
channelName?: string;
|
||||
channelNumber?: number;
|
||||
epgTitle?: string;
|
||||
epgStart?: string;
|
||||
epgEnd?: string;
|
||||
supportsVolume?: boolean;
|
||||
volume?: number;
|
||||
muted?: boolean;
|
||||
}) => void;
|
||||
// Player error notifications
|
||||
onPlayerError?: (
|
||||
callback: (data: {
|
||||
player: string;
|
||||
error: string;
|
||||
originalError: string;
|
||||
}) => void
|
||||
) => void;
|
||||
onEmbeddedMpvSessionUpdate?: (
|
||||
callback: (data: EmbeddedMpvSession) => void
|
||||
) => () => void;
|
||||
onExternalPlayerSessionUpdate?: (
|
||||
callback: (data: ExternalPlayerSession) => void
|
||||
) => () => void;
|
||||
getEmbeddedMpvSupport: () => Promise<EmbeddedMpvSupport>;
|
||||
prepareEmbeddedMpv?: () => Promise<EmbeddedMpvSupport>;
|
||||
createEmbeddedMpvSession: (
|
||||
bounds: EmbeddedMpvBounds,
|
||||
title?: string,
|
||||
initialVolume?: number
|
||||
) => Promise<EmbeddedMpvSession>;
|
||||
loadEmbeddedMpvPlayback: (
|
||||
sessionId: string,
|
||||
playback: ResolvedPortalPlayback
|
||||
) => Promise<void>;
|
||||
setEmbeddedMpvBounds: (
|
||||
sessionId: string,
|
||||
bounds: EmbeddedMpvBounds
|
||||
) => Promise<void>;
|
||||
setEmbeddedMpvPaused: (
|
||||
sessionId: string,
|
||||
paused: boolean
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
seekEmbeddedMpv: (
|
||||
sessionId: string,
|
||||
seconds: number
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
setEmbeddedMpvVolume: (
|
||||
sessionId: string,
|
||||
volume: number
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
setEmbeddedMpvAudioTrack: (
|
||||
sessionId: string,
|
||||
trackId: number
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
setEmbeddedMpvSubtitleTrack?: (
|
||||
sessionId: string,
|
||||
trackId: number
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
setEmbeddedMpvSpeed?: (
|
||||
sessionId: string,
|
||||
speed: number
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
setEmbeddedMpvAspect?: (
|
||||
sessionId: string,
|
||||
aspect: string
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
startEmbeddedMpvRecording?: (
|
||||
sessionId: string,
|
||||
options: EmbeddedMpvRecordingStartOptions
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
stopEmbeddedMpvRecording?: (
|
||||
sessionId: string
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
getEmbeddedMpvDefaultRecordingFolder?: () => Promise<string>;
|
||||
selectEmbeddedMpvRecordingFolder?: () => Promise<string | null>;
|
||||
disposeEmbeddedMpvSession: (
|
||||
sessionId: string
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
getLocalIpAddresses: () => Promise<string[]>;
|
||||
// EPG progress listener
|
||||
onEpgProgress?: (
|
||||
callback: (data: {
|
||||
url: string;
|
||||
status: 'loading' | 'complete' | 'error';
|
||||
stats?: { totalChannels: number; totalPrograms: number };
|
||||
error?: string;
|
||||
}) => void
|
||||
) => void;
|
||||
// DB save content progress listener
|
||||
onDbSaveContentProgress: (
|
||||
callback: (count: number) => void
|
||||
) => void;
|
||||
removeDbSaveContentProgress: () => void;
|
||||
onDbOperationEvent?: (
|
||||
callback: (data: DbOperationEvent) => void
|
||||
) => () => void;
|
||||
dbDeleteAllPlaylists: (
|
||||
operationId?: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbCancelOperation: (
|
||||
operationId: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
// Playback positions
|
||||
dbSavePlaybackPosition: (
|
||||
playlistId: string,
|
||||
data: {
|
||||
contentXtreamId: number;
|
||||
contentType: 'vod' | 'episode';
|
||||
seriesXtreamId?: number;
|
||||
seasonNumber?: number;
|
||||
episodeNumber?: number;
|
||||
position: number;
|
||||
duration: number;
|
||||
title: string;
|
||||
posterUrl?: string;
|
||||
}
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbGetPlaybackPosition: (
|
||||
playlistId: string,
|
||||
contentXtreamId: number,
|
||||
contentType: 'vod' | 'episode'
|
||||
) => Promise<PlaybackPositionData | null>;
|
||||
dbGetSeriesPlaybackPositions: (
|
||||
playlistId: string,
|
||||
seriesXtreamId: number
|
||||
) => Promise<PlaybackPositionData[]>;
|
||||
dbGetRecentPlaybackPositions: (
|
||||
playlistId: string,
|
||||
limit?: number
|
||||
) => Promise<PlaybackPositionData[]>;
|
||||
dbGetAllPlaybackPositions: (
|
||||
playlistId: string
|
||||
) => Promise<PlaybackPositionData[]>;
|
||||
dbClearAllPlaybackPositions: (
|
||||
playlistId: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbClearPlaybackPosition: (
|
||||
playlistId: string,
|
||||
contentXtreamId: number,
|
||||
contentType: 'vod' | 'episode'
|
||||
) => Promise<{ success: boolean }>;
|
||||
onPlaybackPositionUpdate: (
|
||||
callback: (data: PlaybackPositionData) => void
|
||||
) => () => void;
|
||||
closeExternalPlayerSession: (
|
||||
sessionId: string
|
||||
) => Promise<ExternalPlayerSession | null>;
|
||||
// Downloads
|
||||
downloadsStart: (data: {
|
||||
playlistId: string;
|
||||
xtreamId: number;
|
||||
contentType: 'vod' | 'episode';
|
||||
title: string;
|
||||
url: string;
|
||||
posterUrl?: string;
|
||||
downloadFolder: string;
|
||||
headers?: {
|
||||
userAgent?: string;
|
||||
referer?: string;
|
||||
origin?: string;
|
||||
};
|
||||
seriesXtreamId?: number;
|
||||
seasonNumber?: number;
|
||||
episodeNumber?: number;
|
||||
// Playlist info for auto-creation if needed
|
||||
playlistName?: string;
|
||||
playlistType?:
|
||||
| 'xtream'
|
||||
| 'stalker'
|
||||
| 'm3u-file'
|
||||
| 'm3u-text'
|
||||
| 'm3u-url';
|
||||
serverUrl?: string;
|
||||
portalUrl?: string;
|
||||
macAddress?: string;
|
||||
}) => Promise<{ success: boolean; id?: number; error?: string }>;
|
||||
downloadsCancel: (
|
||||
downloadId: number
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
downloadsRetry: (
|
||||
downloadId: number,
|
||||
downloadFolder: string
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
downloadsRemove: (
|
||||
downloadId: number
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
downloadsGetList: (playlistId?: string) => Promise<DownloadItem[]>;
|
||||
downloadsGet: (downloadId: number) => Promise<DownloadItem | null>;
|
||||
downloadsGetDefaultFolder: () => Promise<string>;
|
||||
downloadsSelectFolder: () => Promise<string | null>;
|
||||
downloadsRevealFile: (
|
||||
filePath: string
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
downloadsPlayFile: (
|
||||
filePath: string
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
downloadsClearCompleted: (
|
||||
playlistId?: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
onDownloadsUpdate: (callback: () => void) => () => void;
|
||||
};
|
||||
electron: ElectronBridgeApi;
|
||||
process: NodeJS.Process;
|
||||
require: NodeRequire;
|
||||
}
|
||||
|
||||
/** Download item from the database */
|
||||
interface DownloadItem {
|
||||
id: number;
|
||||
playlistId: string;
|
||||
xtreamId: number;
|
||||
contentType: 'vod' | 'episode';
|
||||
seriesXtreamId?: number;
|
||||
seasonNumber?: number;
|
||||
episodeNumber?: number;
|
||||
title: string;
|
||||
url: string;
|
||||
fileName?: string;
|
||||
filePath?: string;
|
||||
posterUrl?: string;
|
||||
status: 'queued' | 'downloading' | 'completed' | 'failed' | 'canceled';
|
||||
bytesDownloaded?: number;
|
||||
totalBytes?: number;
|
||||
errorMessage?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ The download manager is a desktop-only feature that layers a curated queue, prog
|
||||
## Global API surface
|
||||
|
||||
- **Preload + types**
|
||||
`apps/electron-backend/src/app/api/main.preload.ts` wires every download IPC command plus the `onDownloadsUpdate` listener to `window.electron`. `global.d.ts` now mirrors those methods, adds playback-position helpers, and exposes `onPlaybackPositionUpdate` / `removePlaybackPositionListener` so Angular can type-check the new APIs. This keeps the renderer typing in sync with the backend implementation.
|
||||
`apps/electron-backend/src/app/api/main.preload.ts` wires every download IPC command plus the `onDownloadsUpdate` listener to `window.electron`. The shared `ElectronBridgeApi` contract in `libs/shared/interfaces/src/lib/electron-api.interface.ts` owns the download and playback-position method types; `global.d.ts` and `apps/web/src/typings.d.ts` reference that contract instead of redeclaring the bridge.
|
||||
|
||||
## Routing and navigation
|
||||
|
||||
|
||||
@@ -18,6 +18,23 @@ Do not re-enable direct Node.js access from Angular code. New desktop-only APIs
|
||||
should be added to the preload bridge and backed by an `ipcMain.handle(...)`
|
||||
owner in the Electron backend.
|
||||
|
||||
## Preload API Type Contract
|
||||
|
||||
The canonical renderer bridge type is
|
||||
`libs/shared/interfaces/src/lib/electron-api.interface.ts`.
|
||||
|
||||
Keep these surfaces in sync when adding or changing a preload method:
|
||||
|
||||
1. `ElectronBridgeApi` in `@iptvnator/shared/interfaces`
|
||||
2. `apps/electron-backend/src/app/api/main.preload.ts`
|
||||
3. the owning `ipcMain.handle(...)` event module
|
||||
4. renderer capability checks or runtime bridge services that consume the method
|
||||
|
||||
`global.d.ts` and `apps/web/src/typings.d.ts` should reference
|
||||
`ElectronBridgeApi` instead of redeclaring `window.electron` method lists.
|
||||
`main.preload.ts` is typed as `ElectronBridgeApi`, so missing or extra preload
|
||||
methods fail typecheck instead of silently drifting from renderer typings.
|
||||
|
||||
## Navigation And External URLs
|
||||
|
||||
The main window owns three navigation gates:
|
||||
|
||||
Vendored
+8
-508
@@ -1,21 +1,9 @@
|
||||
import 'jest-extended';
|
||||
import { EmbeddedMpvBounds } from './libs/shared/interfaces/src/lib/embedded-mpv-session.interface';
|
||||
import { EmbeddedMpvRecordingStartOptions } from './libs/shared/interfaces/src/lib/embedded-mpv-session.interface';
|
||||
import { EmbeddedMpvSession } from './libs/shared/interfaces/src/lib/embedded-mpv-session.interface';
|
||||
import { EmbeddedMpvSupport } from './libs/shared/interfaces/src/lib/embedded-mpv-session.interface';
|
||||
import { ExternalPlayerSession } from './libs/shared/interfaces/src/lib/external-player-session.interface';
|
||||
import { PlaybackPositionData } from './libs/shared/interfaces/src/lib/playback-position.interface';
|
||||
import {
|
||||
XtreamBackupFavoriteItem,
|
||||
XtreamBackupHiddenCategory,
|
||||
XtreamBackupRecentlyViewedItem,
|
||||
} from './libs/shared/interfaces/src/lib/playlist-backup.interface';
|
||||
import {
|
||||
PlaylistRefreshEvent,
|
||||
PlaylistRefreshPayload,
|
||||
} from './libs/shared/interfaces/src/lib/playlist-refresh.interface';
|
||||
import { Playlist } from './libs/shared/interfaces/src/lib/playlist.interface';
|
||||
import { ResolvedPortalPlayback } from './libs/shared/interfaces/src/lib/portal-playback.interface';
|
||||
import type {
|
||||
ElectronBridgeApi,
|
||||
ElectronBridgeDbOperationEvent,
|
||||
ElectronDownloadItem,
|
||||
} from './libs/shared/interfaces/src/lib/electron-api.interface';
|
||||
|
||||
declare module 'video.js' {
|
||||
export interface VideoJsPlayer {
|
||||
@@ -24,507 +12,19 @@ declare module 'video.js' {
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface ElectronDbOperationEvent {
|
||||
operationId?: string;
|
||||
operation: string;
|
||||
playlistId?: string;
|
||||
status: 'started' | 'progress' | 'completed' | 'cancelled' | 'error';
|
||||
phase?: string;
|
||||
current?: number;
|
||||
total?: number;
|
||||
increment?: number;
|
||||
error?: string;
|
||||
}
|
||||
type ElectronDbOperationEvent = ElectronBridgeDbOperationEvent;
|
||||
|
||||
interface Window {
|
||||
__IPTVNATOR_CONFIG__?: {
|
||||
BACKEND_URL?: string;
|
||||
};
|
||||
electron: {
|
||||
onPlaylistRefreshEvent?: (
|
||||
callback: (data: PlaylistRefreshEvent) => void
|
||||
) => () => void;
|
||||
getAppVersion: () => Promise<string>;
|
||||
platform: string;
|
||||
fetchPlaylistByUrl: (
|
||||
url: string,
|
||||
title?: string
|
||||
) => Promise<Playlist>;
|
||||
updatePlaylistFromFilePath: (
|
||||
filePath: string,
|
||||
title: string
|
||||
) => Promise<Playlist>;
|
||||
openPlaylistFromFile: () => Promise<Playlist>;
|
||||
saveFileDialog: (
|
||||
defaultPath: string,
|
||||
filters?: { name: string; extensions: string[] }[]
|
||||
) => Promise<string | null>;
|
||||
writeFile: (
|
||||
filePath: string,
|
||||
content: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
setUserAgent: (
|
||||
userAgent?: string | null,
|
||||
referer?: string | null,
|
||||
scopeUrl?: string | null
|
||||
) => Promise<boolean>;
|
||||
openInMpv: (
|
||||
url: string,
|
||||
title: string,
|
||||
thumbnail: string,
|
||||
userAgent: string | undefined,
|
||||
referer?: string,
|
||||
origin?: string,
|
||||
contentInfo?: any,
|
||||
startTime?: number,
|
||||
headers?: Record<string, string>
|
||||
) => Promise<ExternalPlayerSession>;
|
||||
openInVlc: (
|
||||
url: string,
|
||||
title: string,
|
||||
thumbnail: string,
|
||||
userAgent: string | undefined,
|
||||
referer?: string,
|
||||
origin?: string,
|
||||
contentInfo?: any,
|
||||
startTime?: number,
|
||||
headers?: Record<string, string>
|
||||
) => Promise<ExternalPlayerSession>;
|
||||
autoUpdatePlaylists: (playlists: Playlist[]) => Promise<Playlist[]>;
|
||||
fetchEpg: (
|
||||
urls: string[]
|
||||
) => Promise<{ success: boolean; message?: string }>;
|
||||
getChannelPrograms: (channelId: string) => Promise<any>;
|
||||
getCurrentProgramsBatch: (
|
||||
channelIds: string[]
|
||||
) => Promise<Record<string, any>>;
|
||||
getEpgChannels: () => Promise<any>;
|
||||
getEpgChannelsByRange: (
|
||||
skip: number,
|
||||
limit: number
|
||||
) => Promise<any>;
|
||||
forceFetchEpg: (
|
||||
url: string
|
||||
) => Promise<{ success: boolean; message?: string }>;
|
||||
clearEpgData: () => Promise<{ success: boolean }>;
|
||||
checkEpgFreshness: (
|
||||
urls: string[],
|
||||
maxAgeHours?: number
|
||||
) => Promise<{ staleUrls: string[]; freshUrls: string[] }>;
|
||||
searchEpgPrograms: (
|
||||
searchTerm: string,
|
||||
limit?: number
|
||||
) => Promise<any[]>;
|
||||
updateSettings: (settings: any) => Promise<void>;
|
||||
getAiSettings: () => Promise<{
|
||||
aiProvider: string;
|
||||
aiModelName: string;
|
||||
aiApiKey: string;
|
||||
}>;
|
||||
setMpvPlayerPath: (mpvPlayerPath: string) => Promise<void>;
|
||||
setVlcPlayerPath: (vlcPlayerPath: string) => Promise<void>;
|
||||
onExternalPlayerSessionUpdate?: (
|
||||
callback: (data: ExternalPlayerSession) => void
|
||||
) => () => void;
|
||||
onEmbeddedMpvSessionUpdate?: (
|
||||
callback: (data: EmbeddedMpvSession) => void
|
||||
) => () => void;
|
||||
closeExternalPlayerSession: (
|
||||
sessionId: string
|
||||
) => Promise<ExternalPlayerSession | null>;
|
||||
getEmbeddedMpvSupport: () => Promise<EmbeddedMpvSupport>;
|
||||
prepareEmbeddedMpv?: () => Promise<EmbeddedMpvSupport>;
|
||||
createEmbeddedMpvSession: (
|
||||
bounds: EmbeddedMpvBounds,
|
||||
title?: string,
|
||||
initialVolume?: number
|
||||
) => Promise<EmbeddedMpvSession>;
|
||||
loadEmbeddedMpvPlayback: (
|
||||
sessionId: string,
|
||||
playback: ResolvedPortalPlayback
|
||||
) => Promise<void>;
|
||||
setEmbeddedMpvBounds: (
|
||||
sessionId: string,
|
||||
bounds: EmbeddedMpvBounds
|
||||
) => Promise<void>;
|
||||
setEmbeddedMpvPaused: (
|
||||
sessionId: string,
|
||||
paused: boolean
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
seekEmbeddedMpv: (
|
||||
sessionId: string,
|
||||
seconds: number
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
setEmbeddedMpvVolume: (
|
||||
sessionId: string,
|
||||
volume: number
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
setEmbeddedMpvAudioTrack: (
|
||||
sessionId: string,
|
||||
trackId: number
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
setEmbeddedMpvSubtitleTrack?: (
|
||||
sessionId: string,
|
||||
trackId: number
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
setEmbeddedMpvSpeed?: (
|
||||
sessionId: string,
|
||||
speed: number
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
setEmbeddedMpvAspect?: (
|
||||
sessionId: string,
|
||||
aspect: string
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
startEmbeddedMpvRecording?: (
|
||||
sessionId: string,
|
||||
options: EmbeddedMpvRecordingStartOptions
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
stopEmbeddedMpvRecording?: (
|
||||
sessionId: string
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
getEmbeddedMpvDefaultRecordingFolder?: () => Promise<string>;
|
||||
selectEmbeddedMpvRecordingFolder?: () => Promise<string | null>;
|
||||
disposeEmbeddedMpvSession: (
|
||||
sessionId: string
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
stalkerRequest: (payload: {
|
||||
url: string;
|
||||
macAddress: string;
|
||||
params: Record<string, string>;
|
||||
token?: string;
|
||||
serialNumber?: string;
|
||||
requestId?: string;
|
||||
}) => Promise<any>;
|
||||
xtreamRequest: (payload: {
|
||||
url: string;
|
||||
params: Record<string, string>;
|
||||
requestId?: string;
|
||||
sessionId?: string;
|
||||
suppressErrorLog?: boolean;
|
||||
}) => Promise<{ payload: any; action: string }>;
|
||||
xtreamCancelSession: (
|
||||
sessionId: string
|
||||
) => Promise<{ success: boolean; cancelled: number }>;
|
||||
refreshPlaylist: (
|
||||
payload: PlaylistRefreshPayload
|
||||
) => Promise<Playlist>;
|
||||
cancelPlaylistRefresh: (
|
||||
operationId: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
// Database operations
|
||||
dbCreatePlaylist: (playlist: any) => Promise<{ success: boolean }>;
|
||||
dbGetPlaylist: (playlistId: string) => Promise<any>;
|
||||
dbUpsertAppPlaylist: (
|
||||
playlist: any
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbUpsertAppPlaylists: (
|
||||
playlists: any[]
|
||||
) => Promise<{ success: boolean; count: number }>;
|
||||
dbGetAppPlaylists: () => Promise<any[]>;
|
||||
dbGetAppPlaylist: (playlistId: string) => Promise<any | null>;
|
||||
dbUpdatePlaylist: (
|
||||
playlistId: string,
|
||||
updates: any
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbDeletePlaylist: (
|
||||
playlistId: string,
|
||||
operationId?: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbDeleteXtreamContent: (
|
||||
playlistId: string,
|
||||
operationId?: string
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
favorites: XtreamBackupFavoriteItem[];
|
||||
recentlyViewed: XtreamBackupRecentlyViewedItem[];
|
||||
hiddenCategories: XtreamBackupHiddenCategory[];
|
||||
}>;
|
||||
dbRestoreXtreamUserData: (
|
||||
playlistId: string,
|
||||
favorites: XtreamBackupFavoriteItem[],
|
||||
recentlyViewed: XtreamBackupRecentlyViewedItem[],
|
||||
operationId?: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbHasCategories: (
|
||||
playlistId: string,
|
||||
type: string
|
||||
) => Promise<boolean>;
|
||||
dbGetCategories: (
|
||||
playlistId: string,
|
||||
type: string
|
||||
) => Promise<any[]>;
|
||||
dbSaveCategories: (
|
||||
playlistId: string,
|
||||
categories: any[],
|
||||
type: string,
|
||||
hiddenCategoryXtreamIds?: number[]
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbGetAllCategories: (
|
||||
playlistId: string,
|
||||
type: string
|
||||
) => Promise<any[]>;
|
||||
dbUpdateCategoryVisibility: (
|
||||
categoryIds: number[],
|
||||
hidden: boolean
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbHasContent: (
|
||||
playlistId: string,
|
||||
type: string
|
||||
) => Promise<boolean>;
|
||||
dbGetContent: (playlistId: string, type: string) => Promise<any[]>;
|
||||
dbSaveContent: (
|
||||
playlistId: string,
|
||||
streams: any[],
|
||||
type: string,
|
||||
operationId?: string
|
||||
) => Promise<{ success: boolean; count: number }>;
|
||||
dbClearXtreamImportCache: (
|
||||
playlistId: string,
|
||||
type: 'live' | 'movie' | 'series'
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbSearchContent: (
|
||||
playlistId: string,
|
||||
searchTerm: string,
|
||||
types: string[],
|
||||
excludeHidden?: boolean
|
||||
) => Promise<any[]>;
|
||||
dbGlobalSearch: (
|
||||
searchTerm: string,
|
||||
types: string[],
|
||||
excludeHidden?: boolean
|
||||
) => Promise<any[]>;
|
||||
dbGetGlobalRecentlyAdded: (
|
||||
kind: 'all' | 'vod' | 'series',
|
||||
limit?: number,
|
||||
playlistType?:
|
||||
| 'xtream'
|
||||
| 'stalker'
|
||||
| 'm3u-file'
|
||||
| 'm3u-text'
|
||||
| 'm3u-url'
|
||||
) => Promise<any[]>;
|
||||
dbGetRecentlyViewed: () => Promise<any[]>;
|
||||
dbClearRecentlyViewed: () => Promise<{ success: boolean }>;
|
||||
// Favorites
|
||||
dbAddFavorite: (
|
||||
contentId: number,
|
||||
playlistId: string,
|
||||
backdropUrl?: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbRemoveFavorite: (
|
||||
contentId: number,
|
||||
playlistId: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbIsFavorite: (
|
||||
contentId: number,
|
||||
playlistId: string
|
||||
) => Promise<boolean>;
|
||||
dbGetFavorites: (playlistId: string) => Promise<any[]>;
|
||||
dbGetGlobalFavorites: () => Promise<any[]>;
|
||||
dbGetAllGlobalFavorites: () => Promise<any[]>;
|
||||
dbReorderGlobalFavorites: (
|
||||
updates: { content_id: number; position: number }[]
|
||||
) => Promise<{ success: boolean }>;
|
||||
// Recently viewed (playlist-specific)
|
||||
dbGetRecentItems: (playlistId: string) => Promise<any[]>;
|
||||
dbAddRecentItem: (
|
||||
contentId: number,
|
||||
playlistId: string,
|
||||
backdropUrl?: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbClearPlaylistRecentItems: (
|
||||
playlistId: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbRemoveRecentItem: (
|
||||
contentId: number,
|
||||
playlistId: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbRemoveRecentItemsBatch: (
|
||||
items: { contentId: number; playlistId: string }[]
|
||||
) => Promise<{ success: boolean; count: number }>;
|
||||
dbGetContentByXtreamId: (
|
||||
xtreamId: number,
|
||||
playlistId: string,
|
||||
contentType?: 'live' | 'movie' | 'series'
|
||||
) => Promise<any | null>;
|
||||
dbSetContentBackdropIfMissing: (
|
||||
contentId: number,
|
||||
backdropUrl?: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbGetAppState: (key: string) => Promise<string | null>;
|
||||
dbSetAppState: (
|
||||
key: string,
|
||||
value: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
// Remote control
|
||||
onChannelChange?: (
|
||||
callback: (data: { direction: 'up' | 'down' }) => void
|
||||
) => () => void;
|
||||
onRemoteControlCommand?: (
|
||||
callback: (data: {
|
||||
type:
|
||||
| 'channel-select-number'
|
||||
| 'volume-up'
|
||||
| 'volume-down'
|
||||
| 'volume-toggle-mute';
|
||||
number?: number;
|
||||
}) => void
|
||||
) => () => void;
|
||||
updateRemoteControlStatus?: (status: {
|
||||
portal: 'm3u' | 'xtream' | 'stalker' | 'unknown';
|
||||
isLiveView: boolean;
|
||||
channelName?: string;
|
||||
channelNumber?: number;
|
||||
epgTitle?: string;
|
||||
epgStart?: string;
|
||||
epgEnd?: string;
|
||||
supportsVolume?: boolean;
|
||||
volume?: number;
|
||||
muted?: boolean;
|
||||
}) => void;
|
||||
// Player error notifications
|
||||
onPlayerError?: (
|
||||
callback: (data: {
|
||||
player: string;
|
||||
error: string;
|
||||
originalError: string;
|
||||
}) => void
|
||||
) => void;
|
||||
getLocalIpAddresses: () => Promise<string[]>;
|
||||
// EPG progress listener
|
||||
onEpgProgress?: (
|
||||
callback: (data: {
|
||||
url: string;
|
||||
status: 'loading' | 'complete' | 'error';
|
||||
stats?: { totalChannels: number; totalPrograms: number };
|
||||
error?: string;
|
||||
}) => void
|
||||
) => void;
|
||||
// DB save content progress listener
|
||||
onDbSaveContentProgress: (
|
||||
callback: (count: number) => void
|
||||
) => void;
|
||||
removeDbSaveContentProgress: () => void;
|
||||
onDbOperationEvent?: (
|
||||
callback: (data: ElectronDbOperationEvent) => void
|
||||
) => () => void;
|
||||
dbDeleteAllPlaylists: (
|
||||
operationId?: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbCancelOperation: (
|
||||
operationId: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
// Playback positions
|
||||
dbSavePlaybackPosition: (
|
||||
playlistId: string,
|
||||
data: any
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbGetPlaybackPosition: (
|
||||
playlistId: string,
|
||||
contentXtreamId: number,
|
||||
contentType: 'vod' | 'episode'
|
||||
) => Promise<any | null>;
|
||||
dbGetSeriesPlaybackPositions: (
|
||||
playlistId: string,
|
||||
seriesXtreamId: number
|
||||
) => Promise<any[]>;
|
||||
dbGetRecentPlaybackPositions: (
|
||||
playlistId: string,
|
||||
limit?: number
|
||||
) => Promise<PlaybackPositionData[]>;
|
||||
dbGetAllPlaybackPositions: (
|
||||
playlistId: string
|
||||
) => Promise<PlaybackPositionData[]>;
|
||||
dbClearAllPlaybackPositions: (
|
||||
playlistId: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
dbClearPlaybackPosition: (
|
||||
playlistId: string,
|
||||
contentXtreamId: number,
|
||||
contentType: 'vod' | 'episode'
|
||||
) => Promise<{ success: boolean }>;
|
||||
onPlaybackPositionUpdate: (
|
||||
callback: (data: any) => void
|
||||
) => () => void;
|
||||
// Downloads
|
||||
downloadsStart: (data: {
|
||||
playlistId: string;
|
||||
xtreamId: number;
|
||||
contentType: 'vod' | 'episode';
|
||||
title: string;
|
||||
url: string;
|
||||
posterUrl?: string;
|
||||
downloadFolder: string;
|
||||
headers?: {
|
||||
userAgent?: string;
|
||||
referer?: string;
|
||||
origin?: string;
|
||||
};
|
||||
seriesXtreamId?: number;
|
||||
seasonNumber?: number;
|
||||
episodeNumber?: number;
|
||||
// Playlist info for auto-creation if needed
|
||||
playlistName?: string;
|
||||
playlistType?:
|
||||
| 'xtream'
|
||||
| 'stalker'
|
||||
| 'm3u-file'
|
||||
| 'm3u-text'
|
||||
| 'm3u-url';
|
||||
serverUrl?: string;
|
||||
portalUrl?: string;
|
||||
macAddress?: string;
|
||||
}) => Promise<{ success: boolean; id?: number; error?: string }>;
|
||||
downloadsCancel: (
|
||||
downloadId: number
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
downloadsRetry: (
|
||||
downloadId: number,
|
||||
downloadFolder: string
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
downloadsRemove: (
|
||||
downloadId: number
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
downloadsGetList: (playlistId?: string) => Promise<DownloadItem[]>;
|
||||
downloadsGet: (downloadId: number) => Promise<DownloadItem | null>;
|
||||
downloadsGetDefaultFolder: () => Promise<string>;
|
||||
downloadsSelectFolder: () => Promise<string | null>;
|
||||
downloadsRevealFile: (
|
||||
filePath: string
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
downloadsPlayFile: (
|
||||
filePath: string
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
downloadsClearCompleted: (
|
||||
playlistId?: string
|
||||
) => Promise<{ success: boolean }>;
|
||||
onDownloadsUpdate: (callback: () => void) => () => void;
|
||||
};
|
||||
electron: ElectronBridgeApi;
|
||||
process: NodeJS.Process;
|
||||
require: NodeRequire;
|
||||
}
|
||||
|
||||
/** Download item from the database */
|
||||
interface DownloadItem {
|
||||
id: number;
|
||||
playlistId: string;
|
||||
xtreamId: number;
|
||||
contentType: 'vod' | 'episode';
|
||||
seriesXtreamId?: number;
|
||||
seasonNumber?: number;
|
||||
episodeNumber?: number;
|
||||
title: string;
|
||||
url: string;
|
||||
fileName?: string;
|
||||
filePath?: string;
|
||||
posterUrl?: string;
|
||||
status: 'queued' | 'downloading' | 'completed' | 'failed' | 'canceled';
|
||||
bytesDownloaded?: number;
|
||||
totalBytes?: number;
|
||||
errorMessage?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
type DownloadItem = ElectronDownloadItem;
|
||||
}
|
||||
|
||||
// SystemJS module definition
|
||||
|
||||
@@ -1,81 +1,41 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import {
|
||||
EpgChannel,
|
||||
ElectronBridgeApi,
|
||||
ElectronBridgeEpgFetchResult,
|
||||
ElectronBridgeEpgProgress,
|
||||
ElectronBridgeEpgProgressStats,
|
||||
ElectronBridgeEpgProgressStatus,
|
||||
ElectronBridgeEpgChannelWithPrograms,
|
||||
ElectronBridgeEpgFreshnessResult,
|
||||
ELECTRON_BRIDGE_EPG_PROGRESS_STATUSES,
|
||||
ElectronBridgeResult,
|
||||
EpgChannelMetadata,
|
||||
EpgProgram,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { RuntimeCapabilitiesService } from '@iptvnator/services';
|
||||
|
||||
export const EPG_IMPORT_STATUS = {
|
||||
Complete: 'complete',
|
||||
Error: 'error',
|
||||
Loading: 'loading',
|
||||
Queued: 'queued',
|
||||
} as const;
|
||||
export const EPG_IMPORT_STATUS = ELECTRON_BRIDGE_EPG_PROGRESS_STATUSES;
|
||||
export type EpgImportStatus = ElectronBridgeEpgProgressStatus;
|
||||
export type EpgImportStats = ElectronBridgeEpgProgressStats;
|
||||
export type EpgImportProgress = ElectronBridgeEpgProgress;
|
||||
export type EpgFetchResult = ElectronBridgeEpgFetchResult;
|
||||
export type EpgFreshnessResult = ElectronBridgeEpgFreshnessResult;
|
||||
export type EpgClearResult = ElectronBridgeResult;
|
||||
|
||||
export type EpgImportStatus =
|
||||
(typeof EPG_IMPORT_STATUS)[keyof typeof EPG_IMPORT_STATUS];
|
||||
|
||||
export interface EpgImportStats {
|
||||
totalChannels: number;
|
||||
totalPrograms: number;
|
||||
}
|
||||
|
||||
export interface EpgImportProgress {
|
||||
url: string;
|
||||
status: EpgImportStatus;
|
||||
stats?: EpgImportStats;
|
||||
error?: string;
|
||||
queuePosition?: number;
|
||||
}
|
||||
|
||||
export interface EpgFetchResult {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
skipped?: string[];
|
||||
}
|
||||
|
||||
export interface EpgFreshnessResult {
|
||||
staleUrls: string[];
|
||||
freshUrls: string[];
|
||||
}
|
||||
|
||||
export interface EpgClearResult {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
type EpgElectronBridge = Partial<
|
||||
{
|
||||
checkEpgFreshness: (
|
||||
urls: string[],
|
||||
maxAgeHours?: number
|
||||
) => Promise<EpgFreshnessResult>;
|
||||
clearEpgData: () => Promise<EpgClearResult>;
|
||||
fetchEpg: (urls: string[]) => Promise<EpgFetchResult>;
|
||||
forceFetchEpg: (url: string) => Promise<EpgFetchResult>;
|
||||
getChannelPrograms: (channelId: string) => Promise<EpgProgram[]>;
|
||||
getCurrentProgramsBatch: (
|
||||
channelIds: string[]
|
||||
) => Promise<Record<string, EpgProgram | null>>;
|
||||
getEpgChannelMetadata: (
|
||||
channelIds: string[]
|
||||
) => Promise<Record<string, EpgChannelMetadata | null>>;
|
||||
getEpgChannelsByRange: (
|
||||
skip: number,
|
||||
limit: number
|
||||
) => Promise<EpgChannel[]>;
|
||||
onEpgProgress: (callback: (data: EpgImportProgress) => void) => void;
|
||||
searchEpgPrograms: (
|
||||
searchTerm: string,
|
||||
limit?: number
|
||||
) => Promise<EpgProgram[]>;
|
||||
}
|
||||
type EpgElectronBridge = Pick<
|
||||
Partial<ElectronBridgeApi>,
|
||||
| 'checkEpgFreshness'
|
||||
| 'clearEpgData'
|
||||
| 'fetchEpg'
|
||||
| 'forceFetchEpg'
|
||||
| 'getChannelPrograms'
|
||||
| 'getCurrentProgramsBatch'
|
||||
| 'getEpgChannelMetadata'
|
||||
| 'getEpgChannelsByRange'
|
||||
| 'onEpgProgress'
|
||||
| 'searchEpgPrograms'
|
||||
>;
|
||||
|
||||
type EpgRuntimeWindow = Window & {
|
||||
electron?: EpgElectronBridge;
|
||||
};
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class EpgRuntimeBridgeService {
|
||||
private readonly runtime = inject(RuntimeCapabilitiesService);
|
||||
@@ -145,8 +105,10 @@ export class EpgRuntimeBridgeService {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return this.bridge?.getChannelPrograms?.(channelId) ??
|
||||
Promise.resolve(null);
|
||||
return (
|
||||
this.bridge?.getChannelPrograms?.(channelId) ??
|
||||
Promise.resolve(null)
|
||||
);
|
||||
}
|
||||
|
||||
getCurrentProgramsBatch(
|
||||
@@ -156,8 +118,10 @@ export class EpgRuntimeBridgeService {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return this.bridge?.getCurrentProgramsBatch?.(channelIds) ??
|
||||
Promise.resolve(null);
|
||||
return (
|
||||
this.bridge?.getCurrentProgramsBatch?.(channelIds) ??
|
||||
Promise.resolve(null)
|
||||
);
|
||||
}
|
||||
|
||||
getChannelMetadata(
|
||||
@@ -167,8 +131,10 @@ export class EpgRuntimeBridgeService {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return this.bridge?.getEpgChannelMetadata?.(channelIds) ??
|
||||
Promise.resolve(null);
|
||||
return (
|
||||
this.bridge?.getEpgChannelMetadata?.(channelIds) ??
|
||||
Promise.resolve(null)
|
||||
);
|
||||
}
|
||||
|
||||
checkFreshness(
|
||||
@@ -179,20 +145,24 @@ export class EpgRuntimeBridgeService {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return this.bridge?.checkEpgFreshness?.(urls, maxAgeHours) ??
|
||||
Promise.resolve(null);
|
||||
return (
|
||||
this.bridge?.checkEpgFreshness?.(urls, maxAgeHours) ??
|
||||
Promise.resolve(null)
|
||||
);
|
||||
}
|
||||
|
||||
getChannelsByRange(
|
||||
skip: number,
|
||||
limit: number
|
||||
): Promise<EpgChannel[] | null> {
|
||||
): Promise<ElectronBridgeEpgChannelWithPrograms[] | null> {
|
||||
if (!this.supportsChannelBrowser) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return this.bridge?.getEpgChannelsByRange?.(skip, limit) ??
|
||||
Promise.resolve(null);
|
||||
return (
|
||||
this.bridge?.getEpgChannelsByRange?.(skip, limit) ??
|
||||
Promise.resolve(null)
|
||||
);
|
||||
}
|
||||
|
||||
searchPrograms(
|
||||
@@ -203,8 +173,10 @@ export class EpgRuntimeBridgeService {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return this.bridge?.searchEpgPrograms?.(searchTerm, limit) ??
|
||||
Promise.resolve(null);
|
||||
return (
|
||||
this.bridge?.searchEpgPrograms?.(searchTerm, limit) ??
|
||||
Promise.resolve(null)
|
||||
);
|
||||
}
|
||||
|
||||
onProgress(callback: (data: EpgImportProgress) => void): void {
|
||||
@@ -220,6 +192,6 @@ export class EpgRuntimeBridgeService {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (window as EpgRuntimeWindow).electron;
|
||||
return window.electron;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ElectronBridgeApi } from '@iptvnator/shared/interfaces';
|
||||
|
||||
export type RuntimeEnvironment = 'electron' | 'pwa';
|
||||
|
||||
type RuntimeElectronBridge = Record<string, unknown>;
|
||||
|
||||
type RuntimeWindow = Window & {
|
||||
electron?: RuntimeElectronBridge;
|
||||
};
|
||||
type RuntimeElectronBridge = Partial<ElectronBridgeApi>;
|
||||
|
||||
const playbackPositionStorageMethods = [
|
||||
'dbSavePlaybackPosition',
|
||||
@@ -242,7 +239,10 @@ export class RuntimeCapabilitiesService {
|
||||
}
|
||||
|
||||
private hasElectronMethod(methodName: string): boolean {
|
||||
return typeof this.electronBridge?.[methodName] === 'function';
|
||||
const bridge = this.electronBridge as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
return typeof bridge?.[methodName] === 'function';
|
||||
}
|
||||
|
||||
private get electronBridge(): RuntimeElectronBridge | undefined {
|
||||
@@ -250,6 +250,6 @@ export class RuntimeCapabilitiesService {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (window as RuntimeWindow).electron;
|
||||
return window.electron;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ export * from './lib/channel.interface';
|
||||
export * from './lib/channel.model';
|
||||
export * from './lib/dev-logger.util';
|
||||
export * from './lib/embedded-mpv-session.interface';
|
||||
export * from './lib/electron-api.interface';
|
||||
export * from './lib/epg-channel-metadata.model';
|
||||
export * from './lib/epg-channel-with-programs.interface';
|
||||
export * from './lib/epg-channel.model';
|
||||
|
||||
@@ -0,0 +1,793 @@
|
||||
import {
|
||||
EmbeddedMpvBounds,
|
||||
EmbeddedMpvRecordingStartOptions,
|
||||
EmbeddedMpvSession,
|
||||
EmbeddedMpvSupport,
|
||||
} from './embedded-mpv-session.interface';
|
||||
import { EpgChannelMetadata } from './epg-channel-metadata.model';
|
||||
import { EpgProgram } from './epg-program.model';
|
||||
import { ExternalPlayerSession } from './external-player-session.interface';
|
||||
import { PlaybackPositionData } from './playback-position.interface';
|
||||
import {
|
||||
XtreamBackupFavoriteItem,
|
||||
XtreamBackupHiddenCategory,
|
||||
XtreamBackupRecentlyViewedItem,
|
||||
} from './playlist-backup.interface';
|
||||
import {
|
||||
PlaylistRefreshEvent,
|
||||
PlaylistRefreshPayload,
|
||||
} from './playlist-refresh.interface';
|
||||
import { Playlist } from './playlist.interface';
|
||||
import {
|
||||
PlayerContentInfo,
|
||||
ResolvedPortalPlayback,
|
||||
} from './portal-playback.interface';
|
||||
import { PortalDebugEvent } from './portal-debug.interface';
|
||||
import { Settings } from './settings.interface';
|
||||
import { XtreamCategory } from './xtream-category.interface';
|
||||
|
||||
export const ELECTRON_BRIDGE_CONTENT_TYPES = {
|
||||
Episode: 'episode',
|
||||
Live: 'live',
|
||||
Movie: 'movie',
|
||||
Series: 'series',
|
||||
Vod: 'vod',
|
||||
} as const;
|
||||
|
||||
export type ElectronBridgeContentType =
|
||||
(typeof ELECTRON_BRIDGE_CONTENT_TYPES)[keyof typeof ELECTRON_BRIDGE_CONTENT_TYPES];
|
||||
|
||||
export type ElectronBridgePortalContentType =
|
||||
| typeof ELECTRON_BRIDGE_CONTENT_TYPES.Live
|
||||
| typeof ELECTRON_BRIDGE_CONTENT_TYPES.Movie
|
||||
| typeof ELECTRON_BRIDGE_CONTENT_TYPES.Series;
|
||||
|
||||
export type ElectronBridgePlaybackContentType =
|
||||
| typeof ELECTRON_BRIDGE_CONTENT_TYPES.Vod
|
||||
| typeof ELECTRON_BRIDGE_CONTENT_TYPES.Episode;
|
||||
|
||||
export const ELECTRON_BRIDGE_PLAYLIST_TYPES = {
|
||||
M3uFile: 'm3u-file',
|
||||
M3uText: 'm3u-text',
|
||||
M3uUrl: 'm3u-url',
|
||||
Stalker: 'stalker',
|
||||
Xtream: 'xtream',
|
||||
} as const;
|
||||
|
||||
export type ElectronBridgePlaylistType =
|
||||
(typeof ELECTRON_BRIDGE_PLAYLIST_TYPES)[keyof typeof ELECTRON_BRIDGE_PLAYLIST_TYPES];
|
||||
|
||||
export const ELECTRON_BRIDGE_EPG_PROGRESS_STATUSES = {
|
||||
Complete: 'complete',
|
||||
Error: 'error',
|
||||
Loading: 'loading',
|
||||
Queued: 'queued',
|
||||
} as const;
|
||||
|
||||
export type ElectronBridgeEpgProgressStatus =
|
||||
(typeof ELECTRON_BRIDGE_EPG_PROGRESS_STATUSES)[keyof typeof ELECTRON_BRIDGE_EPG_PROGRESS_STATUSES];
|
||||
|
||||
export const ELECTRON_BRIDGE_DB_OPERATION_STATUSES = {
|
||||
Cancelled: 'cancelled',
|
||||
Completed: 'completed',
|
||||
Error: 'error',
|
||||
Progress: 'progress',
|
||||
Started: 'started',
|
||||
} as const;
|
||||
|
||||
export type ElectronBridgeDbOperationStatus =
|
||||
(typeof ELECTRON_BRIDGE_DB_OPERATION_STATUSES)[keyof typeof ELECTRON_BRIDGE_DB_OPERATION_STATUSES];
|
||||
|
||||
export const ELECTRON_BRIDGE_REMOTE_CONTROL_COMMAND_TYPES = {
|
||||
ChannelSelectNumber: 'channel-select-number',
|
||||
VolumeDown: 'volume-down',
|
||||
VolumeToggleMute: 'volume-toggle-mute',
|
||||
VolumeUp: 'volume-up',
|
||||
} as const;
|
||||
|
||||
export type ElectronBridgeRemoteControlCommandType =
|
||||
(typeof ELECTRON_BRIDGE_REMOTE_CONTROL_COMMAND_TYPES)[keyof typeof ELECTRON_BRIDGE_REMOTE_CONTROL_COMMAND_TYPES];
|
||||
|
||||
export const ELECTRON_BRIDGE_REMOTE_PORTALS = {
|
||||
M3u: 'm3u',
|
||||
Stalker: 'stalker',
|
||||
Unknown: 'unknown',
|
||||
Xtream: 'xtream',
|
||||
} as const;
|
||||
|
||||
export type ElectronBridgeRemotePortal =
|
||||
(typeof ELECTRON_BRIDGE_REMOTE_PORTALS)[keyof typeof ELECTRON_BRIDGE_REMOTE_PORTALS];
|
||||
|
||||
export const ELECTRON_BRIDGE_DOWNLOAD_STATUSES = {
|
||||
Canceled: 'canceled',
|
||||
Completed: 'completed',
|
||||
Downloading: 'downloading',
|
||||
Failed: 'failed',
|
||||
Queued: 'queued',
|
||||
} as const;
|
||||
|
||||
export type ElectronBridgeDownloadStatus =
|
||||
(typeof ELECTRON_BRIDGE_DOWNLOAD_STATUSES)[keyof typeof ELECTRON_BRIDGE_DOWNLOAD_STATUSES];
|
||||
|
||||
export const ELECTRON_BRIDGE_GLOBAL_RECENTLY_ADDED_KINDS = {
|
||||
All: 'all',
|
||||
Series: 'series',
|
||||
Vod: 'vod',
|
||||
} as const;
|
||||
|
||||
export type ElectronBridgeGlobalRecentlyAddedKind =
|
||||
(typeof ELECTRON_BRIDGE_GLOBAL_RECENTLY_ADDED_KINDS)[keyof typeof ELECTRON_BRIDGE_GLOBAL_RECENTLY_ADDED_KINDS];
|
||||
|
||||
export interface ElectronBridgeResult {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeErrorResult extends ElectronBridgeResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeCountResult extends ElectronBridgeResult {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeDialogFilter {
|
||||
name: string;
|
||||
extensions: string[];
|
||||
}
|
||||
|
||||
export interface ElectronBridgeAiSettings {
|
||||
aiProvider: string;
|
||||
aiModelName: string;
|
||||
aiApiKey: string;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeStalkerRequestPayload {
|
||||
url: string;
|
||||
macAddress: string;
|
||||
params: Record<string, string>;
|
||||
token?: string;
|
||||
serialNumber?: string;
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeXtreamRequestPayload {
|
||||
url: string;
|
||||
params: Record<string, string>;
|
||||
requestId?: string;
|
||||
sessionId?: string;
|
||||
suppressErrorLog?: boolean;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeXtreamResponse {
|
||||
payload: unknown;
|
||||
action: string;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeXtreamCancelResult extends ElectronBridgeResult {
|
||||
cancelled: number;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeXtreamProbeResult {
|
||||
status: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeEpgFetchResult extends ElectronBridgeResult {
|
||||
message?: string;
|
||||
skipped?: string[];
|
||||
}
|
||||
|
||||
export interface ElectronBridgeEpgFreshnessResult {
|
||||
staleUrls: string[];
|
||||
freshUrls: string[];
|
||||
}
|
||||
|
||||
export interface ElectronBridgeEpgProgressStats {
|
||||
totalChannels: number;
|
||||
totalPrograms: number;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeEpgProgress {
|
||||
url: string;
|
||||
status: ElectronBridgeEpgProgressStatus;
|
||||
stats?: ElectronBridgeEpgProgressStats;
|
||||
error?: string;
|
||||
queuePosition?: number;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeEpgChannelSummary {
|
||||
id: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeEpgChannelListResult {
|
||||
channels: ElectronBridgeEpgChannelSummary[];
|
||||
/** Always empty for this endpoint; retained for wire-format compatibility. */
|
||||
programs: [];
|
||||
}
|
||||
|
||||
export interface ElectronBridgeEpgChannelWithPrograms extends ElectronBridgeEpgChannelSummary {
|
||||
iconUrl: string | null;
|
||||
programs: EpgProgram[];
|
||||
}
|
||||
|
||||
export interface ElectronBridgeDbOperationEvent {
|
||||
operationId?: string;
|
||||
operation: string;
|
||||
playlistId?: string;
|
||||
status: ElectronBridgeDbOperationStatus;
|
||||
phase?: string;
|
||||
current?: number;
|
||||
total?: number;
|
||||
increment?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ElectronBridgePlaylistInput {
|
||||
id?: string;
|
||||
_id?: string;
|
||||
name?: string;
|
||||
title?: string;
|
||||
serverUrl?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
macAddress?: string;
|
||||
url?: string;
|
||||
type?: ElectronBridgePlaylistType | string;
|
||||
lastUpdated?: string;
|
||||
}
|
||||
|
||||
export interface ElectronBridgePlaylistRow {
|
||||
id: string;
|
||||
name: string;
|
||||
serverUrl: string;
|
||||
username: string;
|
||||
password: string;
|
||||
type: string;
|
||||
macAddress?: string;
|
||||
url?: string;
|
||||
lastUpdated?: string;
|
||||
}
|
||||
|
||||
export type ElectronBridgePlaylistUpsertInput =
|
||||
| Playlist
|
||||
| ElectronBridgePlaylistInput;
|
||||
|
||||
export interface ElectronBridgeCategoryRow {
|
||||
id: number;
|
||||
name: string;
|
||||
playlist_id: string;
|
||||
type: 'movies' | 'live' | 'series';
|
||||
xtream_id: number;
|
||||
hidden: boolean;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeXtreamContent {
|
||||
id: number;
|
||||
category_id: number;
|
||||
title: string;
|
||||
rating: string;
|
||||
added: string;
|
||||
poster_url: string;
|
||||
backdrop_url?: string | null;
|
||||
epg_channel_id?: string | null;
|
||||
tv_archive?: number | null;
|
||||
tv_archive_duration?: number | null;
|
||||
direct_source?: string | null;
|
||||
xtream_id: number;
|
||||
type: string;
|
||||
added_at?: string;
|
||||
viewed_at?: string;
|
||||
position?: number | null;
|
||||
}
|
||||
|
||||
export type ElectronBridgeXtreamContentStream =
|
||||
| {
|
||||
category_id: string | number;
|
||||
rating?: string | number;
|
||||
rating_imdb?: string;
|
||||
last_modified?: string;
|
||||
added?: string;
|
||||
stream_icon?: string;
|
||||
poster?: string;
|
||||
cover?: string;
|
||||
name?: string;
|
||||
title?: string;
|
||||
epg_channel_id?: string;
|
||||
tv_archive?: string | number;
|
||||
tv_archive_duration?: string | number;
|
||||
direct_source?: string;
|
||||
series_id?: string | number;
|
||||
stream_id?: string | number;
|
||||
}
|
||||
| Record<string, unknown>;
|
||||
|
||||
export interface ElectronBridgeGlobalSearchResult extends ElectronBridgeXtreamContent {
|
||||
playlist_id: string;
|
||||
playlist_name: string;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeGlobalRecentItem extends ElectronBridgeXtreamContent {
|
||||
playlist_id: string;
|
||||
playlist_name: string;
|
||||
viewed_at: string;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeGlobalFavoriteItem extends ElectronBridgeXtreamContent {
|
||||
playlist_id: string;
|
||||
playlist_name: string;
|
||||
added_at: string;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeGlobalRecentlyAddedItem extends ElectronBridgeXtreamContent {
|
||||
playlist_id: string;
|
||||
playlist_name: string;
|
||||
added_at: string;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeFavoriteReorderUpdate {
|
||||
content_id: number;
|
||||
position: number;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeRecentItemsBatchItem {
|
||||
contentId: number;
|
||||
playlistId: string;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeRemoteControlCommand {
|
||||
type: ElectronBridgeRemoteControlCommandType;
|
||||
number?: number;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeRemoteControlStatus {
|
||||
portal: ElectronBridgeRemotePortal;
|
||||
isLiveView: boolean;
|
||||
channelName?: string;
|
||||
channelNumber?: number;
|
||||
epgTitle?: string;
|
||||
epgStart?: string;
|
||||
epgEnd?: string;
|
||||
supportsVolume?: boolean;
|
||||
volume?: number;
|
||||
muted?: boolean;
|
||||
}
|
||||
|
||||
export interface ElectronBridgePlayerError {
|
||||
player: string;
|
||||
error: string;
|
||||
originalError: string;
|
||||
}
|
||||
|
||||
export interface ElectronBridgePlaybackPositionInput extends Omit<
|
||||
PlaybackPositionData,
|
||||
'playlistId' | 'updatedAt'
|
||||
> {
|
||||
playlistType?: ElectronBridgePlaylistType;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeDownloadHeaders {
|
||||
userAgent?: string;
|
||||
referer?: string;
|
||||
origin?: string;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeDownloadStartPayload {
|
||||
playlistId: string;
|
||||
xtreamId: number;
|
||||
contentType: ElectronBridgePlaybackContentType;
|
||||
title: string;
|
||||
url: string;
|
||||
posterUrl?: string;
|
||||
downloadFolder: string;
|
||||
headers?: ElectronBridgeDownloadHeaders;
|
||||
seriesXtreamId?: number;
|
||||
seasonNumber?: number;
|
||||
episodeNumber?: number;
|
||||
playlistName?: string;
|
||||
playlistType?: ElectronBridgePlaylistType;
|
||||
serverUrl?: string;
|
||||
portalUrl?: string;
|
||||
macAddress?: string;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeDownloadStartResult extends ElectronBridgeErrorResult {
|
||||
id?: number;
|
||||
}
|
||||
|
||||
export interface ElectronDownloadItem {
|
||||
id: number;
|
||||
playlistId: string;
|
||||
xtreamId: number;
|
||||
contentType: ElectronBridgePlaybackContentType;
|
||||
seriesXtreamId?: number;
|
||||
seasonNumber?: number;
|
||||
episodeNumber?: number;
|
||||
title: string;
|
||||
url: string;
|
||||
fileName?: string;
|
||||
filePath?: string;
|
||||
posterUrl?: string;
|
||||
status: ElectronBridgeDownloadStatus;
|
||||
bytesDownloaded?: number;
|
||||
totalBytes?: number;
|
||||
errorMessage?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeApi {
|
||||
onPortalDebugEvent?: (
|
||||
callback: (data: PortalDebugEvent) => void
|
||||
) => () => void;
|
||||
onPlaylistRefreshEvent?: (
|
||||
callback: (data: PlaylistRefreshEvent) => void
|
||||
) => () => void;
|
||||
getAppVersion: () => Promise<string>;
|
||||
platform: string;
|
||||
fetchPlaylistByUrl: (url: string, title?: string) => Promise<Playlist>;
|
||||
updatePlaylistFromFilePath: (
|
||||
filePath: string,
|
||||
title: string
|
||||
) => Promise<Playlist>;
|
||||
openPlaylistFromFile: () => Promise<Playlist | null>;
|
||||
getPathForFile: (file: File) => string;
|
||||
saveFileDialog: (
|
||||
defaultPath: string,
|
||||
filters?: ElectronBridgeDialogFilter[]
|
||||
) => Promise<string | null>;
|
||||
writeFile: (
|
||||
filePath: string,
|
||||
content: string
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
setUserAgent: (
|
||||
userAgent?: string | null,
|
||||
referer?: string | null,
|
||||
scopeUrl?: string | null
|
||||
) => Promise<boolean>;
|
||||
openInMpv: (
|
||||
url: string,
|
||||
title: string,
|
||||
thumbnail: string,
|
||||
userAgent: string | undefined,
|
||||
referer?: string,
|
||||
origin?: string,
|
||||
contentInfo?: PlayerContentInfo,
|
||||
startTime?: number,
|
||||
headers?: Record<string, string>
|
||||
) => Promise<ExternalPlayerSession>;
|
||||
openInVlc: (
|
||||
url: string,
|
||||
title: string,
|
||||
thumbnail: string,
|
||||
userAgent: string | undefined,
|
||||
referer?: string,
|
||||
origin?: string,
|
||||
contentInfo?: PlayerContentInfo,
|
||||
startTime?: number,
|
||||
headers?: Record<string, string>
|
||||
) => Promise<ExternalPlayerSession>;
|
||||
autoUpdatePlaylists: (playlists: Playlist[]) => Promise<Playlist[]>;
|
||||
fetchEpg: (urls: string[]) => Promise<ElectronBridgeEpgFetchResult>;
|
||||
getChannelPrograms: (channelId: string) => Promise<EpgProgram[]>;
|
||||
getCurrentProgramsBatch: (
|
||||
channelIds: string[]
|
||||
) => Promise<Record<string, EpgProgram | null>>;
|
||||
getEpgChannelMetadata: (
|
||||
channelIds: string[]
|
||||
) => Promise<Record<string, EpgChannelMetadata | null>>;
|
||||
getEpgChannels: () => Promise<ElectronBridgeEpgChannelListResult>;
|
||||
getEpgChannelsByRange: (
|
||||
skip: number,
|
||||
limit: number
|
||||
) => Promise<ElectronBridgeEpgChannelWithPrograms[]>;
|
||||
forceFetchEpg: (url: string) => Promise<ElectronBridgeEpgFetchResult>;
|
||||
clearEpgData: () => Promise<ElectronBridgeResult>;
|
||||
checkEpgFreshness: (
|
||||
urls: string[],
|
||||
maxAgeHours?: number
|
||||
) => Promise<ElectronBridgeEpgFreshnessResult>;
|
||||
searchEpgPrograms: (
|
||||
searchTerm: string,
|
||||
limit?: number
|
||||
) => Promise<EpgProgram[]>;
|
||||
updateSettings: (settings: Partial<Settings>) => Promise<void>;
|
||||
getAiSettings: () => Promise<ElectronBridgeAiSettings>;
|
||||
setMpvPlayerPath: (mpvPlayerPath: string) => Promise<void>;
|
||||
setVlcPlayerPath: (vlcPlayerPath: string) => Promise<void>;
|
||||
stalkerRequest: (
|
||||
payload: ElectronBridgeStalkerRequestPayload
|
||||
) => Promise<Record<string, unknown>>;
|
||||
xtreamRequest: (
|
||||
payload: ElectronBridgeXtreamRequestPayload
|
||||
) => Promise<ElectronBridgeXtreamResponse>;
|
||||
xtreamCancelSession: (
|
||||
sessionId: string
|
||||
) => Promise<ElectronBridgeXtreamCancelResult>;
|
||||
xtreamProbeUrl: (
|
||||
url: string,
|
||||
method?: 'GET' | 'HEAD'
|
||||
) => Promise<ElectronBridgeXtreamProbeResult>;
|
||||
refreshPlaylist: (payload: PlaylistRefreshPayload) => Promise<Playlist>;
|
||||
cancelPlaylistRefresh: (
|
||||
operationId: string
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
dbCreatePlaylist: (
|
||||
playlist: ElectronBridgePlaylistUpsertInput
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
dbGetPlaylist: (
|
||||
playlistId: string
|
||||
) => Promise<ElectronBridgePlaylistRow | null>;
|
||||
dbUpsertAppPlaylist: (playlist: Playlist) => Promise<ElectronBridgeResult>;
|
||||
dbUpsertAppPlaylists: (
|
||||
playlists: Playlist[]
|
||||
) => Promise<ElectronBridgeCountResult>;
|
||||
dbGetAppPlaylists: () => Promise<Playlist[]>;
|
||||
dbGetAppPlaylist: (playlistId: string) => Promise<Playlist | null>;
|
||||
dbUpdatePlaylist: (
|
||||
playlistId: string,
|
||||
updates: Partial<Playlist> | ElectronBridgePlaylistInput
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
dbDeletePlaylist: (
|
||||
playlistId: string,
|
||||
operationId?: string
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
dbDeleteXtreamContent: (
|
||||
playlistId: string,
|
||||
operationId?: string
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
favorites: XtreamBackupFavoriteItem[];
|
||||
recentlyViewed: XtreamBackupRecentlyViewedItem[];
|
||||
hiddenCategories: XtreamBackupHiddenCategory[];
|
||||
}>;
|
||||
dbRestoreXtreamUserData: (
|
||||
playlistId: string,
|
||||
favorites: XtreamBackupFavoriteItem[],
|
||||
recentlyViewed: XtreamBackupRecentlyViewedItem[],
|
||||
operationId?: string
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
dbHasCategories: (playlistId: string, type: string) => Promise<boolean>;
|
||||
dbGetCategories: (
|
||||
playlistId: string,
|
||||
type: string
|
||||
) => Promise<ElectronBridgeCategoryRow[]>;
|
||||
dbSaveCategories: (
|
||||
playlistId: string,
|
||||
categories: XtreamCategory[],
|
||||
type: string,
|
||||
hiddenCategoryXtreamIds?: number[]
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
dbGetAllCategories: (
|
||||
playlistId: string,
|
||||
type: string
|
||||
) => Promise<ElectronBridgeCategoryRow[]>;
|
||||
dbUpdateCategoryVisibility: (
|
||||
categoryIds: number[],
|
||||
hidden: boolean
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
dbHasContent: (playlistId: string, type: string) => Promise<boolean>;
|
||||
dbGetContent: (
|
||||
playlistId: string,
|
||||
type: string
|
||||
) => Promise<ElectronBridgeXtreamContent[]>;
|
||||
dbSaveContent: (
|
||||
playlistId: string,
|
||||
streams: ElectronBridgeXtreamContentStream[],
|
||||
type: string,
|
||||
operationId?: string
|
||||
) => Promise<ElectronBridgeCountResult>;
|
||||
dbClearXtreamImportCache: (
|
||||
playlistId: string,
|
||||
type: ElectronBridgePortalContentType
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
dbSearchContent: (
|
||||
playlistId: string,
|
||||
searchTerm: string,
|
||||
types: string[],
|
||||
excludeHidden?: boolean
|
||||
) => Promise<ElectronBridgeXtreamContent[]>;
|
||||
dbGlobalSearch: (
|
||||
searchTerm: string,
|
||||
types: string[],
|
||||
excludeHidden?: boolean
|
||||
) => Promise<ElectronBridgeGlobalSearchResult[]>;
|
||||
dbGetGlobalRecentlyAdded: (
|
||||
kind: ElectronBridgeGlobalRecentlyAddedKind,
|
||||
limit?: number,
|
||||
playlistType?: ElectronBridgePlaylistType
|
||||
) => Promise<ElectronBridgeGlobalRecentlyAddedItem[]>;
|
||||
dbGetRecentlyViewed: () => Promise<ElectronBridgeGlobalRecentItem[]>;
|
||||
dbClearRecentlyViewed: () => Promise<ElectronBridgeResult>;
|
||||
dbAddFavorite: (
|
||||
contentId: number,
|
||||
playlistId: string,
|
||||
backdropUrl?: string
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
dbRemoveFavorite: (
|
||||
contentId: number,
|
||||
playlistId: string
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
dbIsFavorite: (contentId: number, playlistId: string) => Promise<boolean>;
|
||||
dbGetFavorites: (
|
||||
playlistId: string
|
||||
) => Promise<ElectronBridgeXtreamContent[]>;
|
||||
dbGetGlobalFavorites: () => Promise<ElectronBridgeGlobalFavoriteItem[]>;
|
||||
dbGetAllGlobalFavorites: () => Promise<ElectronBridgeGlobalFavoriteItem[]>;
|
||||
dbReorderGlobalFavorites: (
|
||||
updates: ElectronBridgeFavoriteReorderUpdate[]
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
dbGetRecentItems: (
|
||||
playlistId: string
|
||||
) => Promise<ElectronBridgeXtreamContent[]>;
|
||||
dbAddRecentItem: (
|
||||
contentId: number,
|
||||
playlistId: string,
|
||||
backdropUrl?: string
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
dbClearPlaylistRecentItems: (
|
||||
playlistId: string
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
dbRemoveRecentItem: (
|
||||
contentId: number,
|
||||
playlistId: string
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
dbRemoveRecentItemsBatch: (
|
||||
items: ElectronBridgeRecentItemsBatchItem[]
|
||||
) => Promise<ElectronBridgeCountResult>;
|
||||
dbGetContentByXtreamId: (
|
||||
xtreamId: number,
|
||||
playlistId: string,
|
||||
contentType?: ElectronBridgePortalContentType
|
||||
) => Promise<ElectronBridgeXtreamContent | null>;
|
||||
dbSetContentBackdropIfMissing: (
|
||||
contentId: number,
|
||||
backdropUrl?: string
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
dbGetAppState: (key: string) => Promise<string | null>;
|
||||
dbSetAppState: (
|
||||
key: string,
|
||||
value: string
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
onChannelChange?: (
|
||||
callback: (data: { direction: 'up' | 'down' }) => void
|
||||
) => () => void;
|
||||
onRemoteControlCommand?: (
|
||||
callback: (data: ElectronBridgeRemoteControlCommand) => void
|
||||
) => () => void;
|
||||
updateRemoteControlStatus?: (
|
||||
status: ElectronBridgeRemoteControlStatus
|
||||
) => void;
|
||||
onPlayerError?: (
|
||||
callback: (data: ElectronBridgePlayerError) => void
|
||||
) => void;
|
||||
getLocalIpAddresses: () => Promise<string[]>;
|
||||
onEpgProgress?: (
|
||||
callback: (data: ElectronBridgeEpgProgress) => void
|
||||
) => void;
|
||||
onDbSaveContentProgress: (callback: (count: number) => void) => void;
|
||||
removeDbSaveContentProgress: () => void;
|
||||
onDbOperationEvent?: (
|
||||
callback: (data: ElectronBridgeDbOperationEvent) => void
|
||||
) => () => void;
|
||||
dbDeleteAllPlaylists: (
|
||||
operationId?: string
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
dbCancelOperation: (operationId: string) => Promise<ElectronBridgeResult>;
|
||||
dbSavePlaybackPosition: (
|
||||
playlistId: string,
|
||||
data: ElectronBridgePlaybackPositionInput
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
dbGetPlaybackPosition: (
|
||||
playlistId: string,
|
||||
contentXtreamId: number,
|
||||
contentType: ElectronBridgePlaybackContentType
|
||||
) => Promise<PlaybackPositionData | null>;
|
||||
dbGetSeriesPlaybackPositions: (
|
||||
playlistId: string,
|
||||
seriesXtreamId: number
|
||||
) => Promise<PlaybackPositionData[]>;
|
||||
dbGetRecentPlaybackPositions: (
|
||||
playlistId: string,
|
||||
limit?: number
|
||||
) => Promise<PlaybackPositionData[]>;
|
||||
dbGetAllPlaybackPositions: (
|
||||
playlistId: string
|
||||
) => Promise<PlaybackPositionData[]>;
|
||||
dbClearAllPlaybackPositions: (
|
||||
playlistId: string
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
dbClearPlaybackPosition: (
|
||||
playlistId: string,
|
||||
contentXtreamId: number,
|
||||
contentType: ElectronBridgePlaybackContentType
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
onPlaybackPositionUpdate: (
|
||||
callback: (data: PlaybackPositionData) => void
|
||||
) => () => void;
|
||||
onExternalPlayerSessionUpdate?: (
|
||||
callback: (data: ExternalPlayerSession) => void
|
||||
) => () => void;
|
||||
onEmbeddedMpvSessionUpdate?: (
|
||||
callback: (data: EmbeddedMpvSession) => void
|
||||
) => () => void;
|
||||
closeExternalPlayerSession: (
|
||||
sessionId: string
|
||||
) => Promise<ExternalPlayerSession | null>;
|
||||
getEmbeddedMpvSupport: () => Promise<EmbeddedMpvSupport>;
|
||||
prepareEmbeddedMpv?: () => Promise<EmbeddedMpvSupport>;
|
||||
createEmbeddedMpvSession: (
|
||||
bounds: EmbeddedMpvBounds,
|
||||
title?: string,
|
||||
initialVolume?: number
|
||||
) => Promise<EmbeddedMpvSession>;
|
||||
loadEmbeddedMpvPlayback: (
|
||||
sessionId: string,
|
||||
playback: ResolvedPortalPlayback
|
||||
) => Promise<void>;
|
||||
setEmbeddedMpvBounds: (
|
||||
sessionId: string,
|
||||
bounds: EmbeddedMpvBounds
|
||||
) => Promise<void>;
|
||||
setEmbeddedMpvPaused: (
|
||||
sessionId: string,
|
||||
paused: boolean
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
seekEmbeddedMpv: (
|
||||
sessionId: string,
|
||||
seconds: number
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
setEmbeddedMpvVolume: (
|
||||
sessionId: string,
|
||||
volume: number
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
setEmbeddedMpvAudioTrack: (
|
||||
sessionId: string,
|
||||
trackId: number
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
setEmbeddedMpvSubtitleTrack?: (
|
||||
sessionId: string,
|
||||
trackId: number
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
setEmbeddedMpvSpeed?: (
|
||||
sessionId: string,
|
||||
speed: number
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
setEmbeddedMpvAspect?: (
|
||||
sessionId: string,
|
||||
aspect: string
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
startEmbeddedMpvRecording?: (
|
||||
sessionId: string,
|
||||
options: EmbeddedMpvRecordingStartOptions
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
stopEmbeddedMpvRecording?: (
|
||||
sessionId: string
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
getEmbeddedMpvDefaultRecordingFolder?: () => Promise<string>;
|
||||
selectEmbeddedMpvRecordingFolder?: () => Promise<string | null>;
|
||||
disposeEmbeddedMpvSession: (
|
||||
sessionId: string
|
||||
) => Promise<EmbeddedMpvSession | null>;
|
||||
downloadsStart: (
|
||||
data: ElectronBridgeDownloadStartPayload
|
||||
) => Promise<ElectronBridgeDownloadStartResult>;
|
||||
downloadsCancel: (downloadId: number) => Promise<ElectronBridgeErrorResult>;
|
||||
downloadsRetry: (
|
||||
downloadId: number,
|
||||
downloadFolder: string
|
||||
) => Promise<ElectronBridgeErrorResult>;
|
||||
downloadsRemove: (downloadId: number) => Promise<ElectronBridgeErrorResult>;
|
||||
downloadsGetList: (playlistId?: string) => Promise<ElectronDownloadItem[]>;
|
||||
downloadsGet: (downloadId: number) => Promise<ElectronDownloadItem | null>;
|
||||
downloadsGetDefaultFolder: () => Promise<string>;
|
||||
downloadsSelectFolder: () => Promise<string | null>;
|
||||
downloadsRevealFile: (
|
||||
filePath: string
|
||||
) => Promise<ElectronBridgeErrorResult>;
|
||||
downloadsPlayFile: (filePath: string) => Promise<ElectronBridgeErrorResult>;
|
||||
downloadsClearCompleted: (
|
||||
playlistId?: string
|
||||
) => Promise<ElectronBridgeResult>;
|
||||
onDownloadsUpdate: (callback: () => void) => () => void;
|
||||
}
|
||||
@@ -25,17 +25,15 @@ import { addDays, differenceInMinutes, format, parse, subDays } from 'date-fns';
|
||||
import { Observable, Subscription, startWith } from 'rxjs';
|
||||
import {
|
||||
Channel,
|
||||
ElectronBridgeEpgChannelWithPrograms,
|
||||
EpgChannel,
|
||||
EpgChannelWithPrograms,
|
||||
EpgProgram,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { EpgRuntimeBridgeService } from '@iptvnator/epg/data-access';
|
||||
import { EpgItemDescriptionComponent } from '../epg-list/epg-item-description/epg-item-description.component';
|
||||
import { COMPONENT_OVERLAY_REF } from './overlay-ref.token';
|
||||
|
||||
interface MultiEpgChannel extends EpgChannelWithPrograms {
|
||||
iconUrl?: string | null;
|
||||
}
|
||||
type MultiEpgChannel = ElectronBridgeEpgChannelWithPrograms;
|
||||
|
||||
interface EnrichedProgram extends EpgProgram {
|
||||
startDate: Date;
|
||||
@@ -63,13 +61,7 @@ export function isSelectedEpgDayToday(
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [
|
||||
DatePipe,
|
||||
MatButtonModule,
|
||||
MatIcon,
|
||||
MatTooltip,
|
||||
TranslatePipe,
|
||||
],
|
||||
imports: [DatePipe, MatButtonModule, MatIcon, MatTooltip, TranslatePipe],
|
||||
selector: 'app-multi-epg-container',
|
||||
templateUrl: './multi-epg-container.component.html',
|
||||
styleUrls: ['./multi-epg-container.component.scss'],
|
||||
@@ -288,10 +280,7 @@ export class MultiEpgContainerComponent
|
||||
|
||||
if (response && Array.isArray(response)) {
|
||||
// Append new data to existing data
|
||||
this.originalEpgData.update((data) => [
|
||||
...data,
|
||||
...(response as MultiEpgChannel[]),
|
||||
]);
|
||||
this.originalEpgData.update((data) => [...data, ...response]);
|
||||
|
||||
// Update isLastPage based on the number of channels received
|
||||
this.isLastPage.set(response.length < this.visibleChannels);
|
||||
@@ -369,7 +358,7 @@ export class MultiEpgContainerComponent
|
||||
/**
|
||||
* Get display name from EpgChannel
|
||||
*/
|
||||
getChannelName(channel: EpgChannel): string {
|
||||
getChannelName(channel: EpgChannel | MultiEpgChannel): string {
|
||||
if (typeof channel.displayName === 'string') {
|
||||
return channel.displayName;
|
||||
}
|
||||
@@ -389,7 +378,11 @@ export class MultiEpgContainerComponent
|
||||
if ('iconUrl' in channel && channel.iconUrl) {
|
||||
return channel.iconUrl;
|
||||
}
|
||||
if (channel.icon && channel.icon.length > 0) {
|
||||
if (
|
||||
'icon' in channel &&
|
||||
Array.isArray(channel.icon) &&
|
||||
channel.icon.length > 0
|
||||
) {
|
||||
return channel.icon[0].src;
|
||||
}
|
||||
return '';
|
||||
@@ -472,10 +465,7 @@ export class MultiEpgContainerComponent
|
||||
this.searchDebounceTimer = setTimeout(async () => {
|
||||
this.isSearchingPrograms.set(true);
|
||||
try {
|
||||
const results = await this.epgBridge.searchPrograms(
|
||||
query,
|
||||
20
|
||||
);
|
||||
const results = await this.epgBridge.searchPrograms(query, 20);
|
||||
this.programSearchResults.set(
|
||||
(results as ProgramSearchResult[]) || []
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user