feat: implement URL probing functionality and enhance time formatting in EPG component

This commit is contained in:
4gray committed 2026-04-05 19:04:20 +02:00
1 parent 826d8d24b1
commit 04e8709d7b
8 files changed
+198 -62

No files matched your search

+3 -5
View File
@@ -44,17 +44,15 @@
"serve": {
"executor": "nx:run-commands",
"options": {
"commands": [
"nx run web:serve",
"nx run electron-backend:serve-electron"
]
"command": "nx run electron-backend:serve-electron"
}
},
"serve-electron": {
"executor": "nx-electron:execute",
"options": {
"buildTarget": "electron-backend:build",
"args": ["--remote-debugging-port=9222"]
"waitUntilTargets": ["web:serve"],
"remoteDebuggingPort": 9222
}
},
"package": {
@@ -254,6 +254,8 @@ contextBridge.exposeInMainWorld('electron', {
}) => ipcRenderer.invoke('XTREAM_REQUEST', payload),
xtreamCancelSession: (sessionId: string) =>
ipcRenderer.invoke('XTREAM_CANCEL_SESSION', sessionId),
xtreamProbeUrl: (url: string, method?: 'GET' | 'HEAD') =>
ipcRenderer.invoke('XTREAM_PROBE_URL', { url, method }),
refreshPlaylist: (payload: PlaylistRefreshPayload) =>
ipcRenderer.invoke('PLAYLIST:REFRESH', payload),
cancelPlaylistRefresh: (operationId: string) =>
+7 -1
View File
@@ -10,6 +10,10 @@ export default class App {
static application: Electron.App;
static BrowserWindow;
private static shouldOpenDevTools() {
return process.env.ELECTRON_OPEN_DEVTOOLS === '1';
}
public static isDevelopmentMode() {
// First check ELECTRON_IS_DEV environment variable (used by E2E tests)
// This allows E2E tests to run in production mode without packaging
@@ -161,7 +165,9 @@ export default class App {
// load the index.html of the app.
if (App.isDevelopmentMode()) {
App.mainWindow.loadURL(`http://localhost:${rendererAppPort}`);
App.mainWindow.webContents.openDevTools();
if (App.shouldOpenDevTools()) {
App.mainWindow.webContents.openDevTools();
}
} else {
App.mainWindow.loadFile(
join(__dirname, '..', rendererAppName, 'index.html')
@@ -44,6 +44,23 @@ function getRecentlyAddedContentTypes(
return ['movie', 'series'];
}
function selectContentFields() {
return {
id: schema.content.id,
category_id: schema.content.categoryId,
title: schema.content.title,
rating: schema.content.rating,
added: schema.content.added,
poster_url: schema.content.posterUrl,
epg_channel_id: schema.content.epgChannelId,
tv_archive: schema.content.tvArchive,
tv_archive_duration: schema.content.tvArchiveDuration,
direct_source: schema.content.directSource,
xtream_id: schema.content.xtreamId,
type: schema.content.type,
};
}
export async function hasContent(
db: AppDatabase,
playlistId: string,
@@ -72,16 +89,7 @@ export async function getContent(
type: 'live' | 'movie' | 'series'
) {
const baseQuery = db
.select({
id: schema.content.id,
category_id: schema.content.categoryId,
title: schema.content.title,
rating: schema.content.rating,
added: schema.content.added,
poster_url: schema.content.posterUrl,
xtream_id: schema.content.xtreamId,
type: schema.content.type,
})
.select(selectContentFields())
.from(schema.content)
.innerJoin(
schema.categories,
@@ -112,15 +120,8 @@ export async function getGlobalRecentlyAdded(
return db
.select({
id: schema.content.id,
category_id: schema.content.categoryId,
title: schema.content.title,
rating: schema.content.rating,
added: schema.content.added,
...selectContentFields(),
added_at: schema.content.added,
poster_url: schema.content.posterUrl,
xtream_id: schema.content.xtreamId,
type: schema.content.type,
playlist_id: schema.playlists.id,
playlist_name: schema.playlists.name,
})
@@ -150,6 +151,10 @@ type XtreamContentValue = {
rating: string;
added: string;
posterUrl: string;
epgChannelId?: string | null;
tvArchive?: number | null;
tvArchiveDuration?: number | null;
directSource?: string | null;
xtreamId: number;
type: 'live' | 'movie' | 'series';
};
@@ -161,12 +166,16 @@ type XtreamContentSource = Record<string, unknown> & {
last_modified?: string;
added?: string;
stream_icon?: string;
poster?: string;
cover?: string;
name?: string;
title?: string;
series_id?: string | number;
stream_id?: string | number;
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;
};
function toXtreamContentValue(
@@ -205,6 +214,25 @@ function toXtreamContentValue(
posterUrl: String(
source.stream_icon || source.poster || source.cover || ''
),
epgChannelId:
type === 'live'
? String(source.epg_channel_id ?? '').trim() || null
: null,
tvArchive:
type === 'live'
? Number.parseInt(String(source.tv_archive ?? '0'), 10) || 0
: null,
tvArchiveDuration:
type === 'live'
? Number.parseInt(
String(source.tv_archive_duration ?? '0'),
10
) || 0
: null,
directSource:
type === 'live'
? String(source.direct_source ?? '').trim() || null
: null,
xtreamId:
type === 'series'
? parseInt(String(source.series_id || '0'))
@@ -343,16 +371,7 @@ export async function getContentByXtreamId(
playlistId: string
) {
const result = await db
.select({
id: schema.content.id,
category_id: schema.content.categoryId,
title: schema.content.title,
rating: schema.content.rating,
added: schema.content.added,
poster_url: schema.content.posterUrl,
xtream_id: schema.content.xtreamId,
type: schema.content.type,
})
.select(selectContentFields())
.from(schema.content)
.innerJoin(
schema.categories,
@@ -397,16 +416,7 @@ export async function searchContent(
}
const candidates = await db
.select({
id: schema.content.id,
category_id: schema.content.categoryId,
title: schema.content.title,
rating: schema.content.rating,
added: schema.content.added,
poster_url: schema.content.posterUrl,
xtream_id: schema.content.xtreamId,
type: schema.content.type,
})
.select(selectContentFields())
.from(schema.content)
.innerJoin(
schema.categories,
@@ -449,14 +459,7 @@ export async function globalSearch(
const candidates = await db
.select({
id: schema.content.id,
category_id: schema.content.categoryId,
title: schema.content.title,
rating: schema.content.rating,
added: schema.content.added,
poster_url: schema.content.posterUrl,
xtream_id: schema.content.xtreamId,
type: schema.content.type,
...selectContentFields(),
playlist_id: schema.categories.playlistId,
playlist_name: schema.playlists.name,
})
@@ -245,3 +245,46 @@ type ActiveXtreamRequest = {
};
const activeXtreamRequests = new Map<string, ActiveXtreamRequest>();
ipcMain.handle(
'XTREAM_PROBE_URL',
async (
_event,
payload: {
url: string;
method?: 'GET' | 'HEAD';
}
) => {
const config: AxiosRequestConfig = {
method: payload.method ?? 'HEAD',
url: payload.url,
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
},
timeout: 10000,
maxRedirects: 5,
validateStatus: () => true,
};
try {
const response = await axios(config);
return {
status: response.status,
url: payload.url,
};
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
return {
status: error.response.status,
url: payload.url,
};
}
return {
status: 0,
url: payload.url,
};
}
}
);