From 4cd7ab0afe4671314982254e88ac137256470d67 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 1 Mar 2025 18:43:56 +0100 Subject: [PATCH] refactor(electron): remove unused preload, HTML, and server files --- electron/api.ts | 639 ----------------------------- electron/epg-worker.html | 12 - electron/epg-worker.ts | 175 -------- electron/main.ts | 154 ------- electron/menu.ts | 151 ------- electron/preload.ts | 7 - electron/remote-control/index.sqrl | 85 ---- electron/server.ts | 86 ---- 8 files changed, 1309 deletions(-) delete mode 100644 electron/api.ts delete mode 100644 electron/epg-worker.html delete mode 100644 electron/epg-worker.ts delete mode 100644 electron/main.ts delete mode 100644 electron/menu.ts delete mode 100644 electron/preload.ts delete mode 100644 electron/remote-control/index.sqrl delete mode 100644 electron/server.ts diff --git a/electron/api.ts b/electron/api.ts deleted file mode 100644 index ff124e51f..000000000 --- a/electron/api.ts +++ /dev/null @@ -1,639 +0,0 @@ -import axios from 'axios'; -import { app, BrowserWindow, ipcMain, session } from 'electron'; -import { promises as fsPromises } from 'fs'; -import { parse } from 'iptv-playlist-parser'; -import { Channel } from '../shared/channel.interface'; -import { - AUTO_UPDATE_PLAYLISTS, - AUTO_UPDATE_PLAYLISTS_RESPONSE, - CHANNEL_SET_USER_AGENT, - DELETE_ALL_PLAYLISTS, - EPG_ERROR, - EPG_FETCH, - EPG_FETCH_DONE, - EPG_FORCE_FETCH, - EPG_GET_CHANNELS, - EPG_GET_CHANNELS_BY_RANGE, - EPG_GET_CHANNELS_BY_RANGE_RESPONSE, - EPG_GET_CHANNELS_DONE, - EPG_GET_PROGRAM, - EPG_GET_PROGRAM_DONE, - ERROR, - IS_PLAYLISTS_MIGRATION_POSSIBLE, - IS_PLAYLISTS_MIGRATION_POSSIBLE_RESPONSE, - MIGRATE_PLAYLISTS, - MIGRATE_PLAYLISTS_RESPONSE, - OPEN_FILE, - OPEN_MPV_PLAYER, - OPEN_VLC_PLAYER, - PLAYLIST_PARSE_BY_URL, - PLAYLIST_PARSE_RESPONSE, - PLAYLIST_UPDATE, - PLAYLIST_UPDATE_RESPONSE, - SET_MPV_PLAYER_PATH, - SET_VLC_PLAYER_PATH, - SETTINGS_UPDATE, - STALKER_REQUEST, - STALKER_RESPONSE, - XTREAM_REQUEST, - XTREAM_RESPONSE, -} from '../shared/ipc-commands'; -import { Playlist } from '../shared/playlist.interface'; -import { createPlaylistObject } from '../shared/playlist.utils'; -import { ParsedPlaylist } from '../src/typings.d'; -import { Server } from './server'; - -const fs = require('fs'); -const https = require('https'); -const child_process = require('child_process'); -const path = require('path'); - -const mpvAPI = require('node-mpv'); - -/** @deprecated - used only for migration */ -const Nedb = require('nedb-promises'); - -/** @deprecated - used only for migration */ -const userData = process.env['e2e'] - ? process.cwd() + '/e2e' - : app.getPath('userData'); - -/** @deprecated - used only for migration */ -const dbPath = `${userData}/db/data.db`; -/** @deprecated - used only for migration */ -const db = new Nedb({ - filename: dbPath, - autoload: true, -}); - -const agent = new https.Agent({ - rejectUnauthorized: false, -}); - -const MPV_PLAYER_PATH = 'MPV_PLAYER_PATH'; -const VLC_PLAYER_PATH = 'VLC_PLAYER_PATH'; - -export class Api { - /** Instance of the main application window */ - mainWindow: BrowserWindow; - - /** Default user agent stored as a fallback value */ - defaultUserAgent: string; - - /** Default referer url value */ - defaultReferer: string; - - /** Instance of the epg browser window */ - workerWindow: BrowserWindow; - - store; - - mpv; - - settings; - - server; - - constructor(store) { - this.server = new Server(this); - this.store = store; - this.mpv = this.createMpvInstance(); - this.mpv - .on('quit', () => (this.mpv = null)) - .on('crash', () => (this.mpv = null)); - - ipcMain - .on(PLAYLIST_PARSE_BY_URL, (event, args) => { - try { - axios - .get(args.url, { httpsAgent: agent }) - .then((result) => { - const parsedPlaylist = this.parsePlaylist( - result.data - ); - const playlistObject = createPlaylistObject( - args.title, - parsedPlaylist, - args.url, - 'URL' - ); - event.sender.send(PLAYLIST_PARSE_RESPONSE, { - payload: playlistObject, - }); - }) - .catch((err) => { - event.sender.send(ERROR, { - message: err.response.statusText, - status: err.response.status, - }); - }); - } catch (err) { - event.sender.send(ERROR, { - message: err.response.statusText, - status: err.response.status, - }); - } - }) - .on(OPEN_FILE, (event, args) => { - fs.readFile( - args.filePath, - 'utf-8', - (err: NodeJS.ErrnoException, data: string) => { - if (err) { - console.log( - 'An error ocurred reading the file :' + - err.message - ); - return; - } - - const parsedPlaylist = this.parsePlaylist(data); - const playlistObject = createPlaylistObject( - args.fileName, - parsedPlaylist, - args.filePath, - 'FILE' - ); - - event.sender.send(PLAYLIST_PARSE_RESPONSE, { - payload: playlistObject, - }); - } - ); - }) - .on( - PLAYLIST_UPDATE, - ( - event, - args: { - id: string; - title: string; - filePath?: string; - url?: string; - } - ) => { - if (args.filePath && args.id) { - this.fetchPlaylistByFilePath(args, event); - } else if (args.url && args.id) { - this.fetchPlaylistByUrl(args, event); - } - } - ) - .on( - CHANNEL_SET_USER_AGENT, - (_event, args: { userAgent: string; referer?: string }) => { - if (args.userAgent || args.referer !== undefined) { - this.setUserAgent( - args.userAgent || 'localhost', - args.referer || 'localhost' - ); - } - } - ) - .on(IS_PLAYLISTS_MIGRATION_POSSIBLE, (event) => { - db.count({ - type: { $exists: false }, - }).then((count: number) => { - event.sender.send( - IS_PLAYLISTS_MIGRATION_POSSIBLE_RESPONSE, - { - result: count > 0, - message: - count > 0 - ? `${count} playlists were found, which can be migrated from the database used in the last version of the application.` - : 'No playlists for migration', - } - ); - }); - }) - .on( - AUTO_UPDATE_PLAYLISTS, - // eslint-disable-next-line @typescript-eslint/no-misused-promises - async (event, playlists: Partial[]) => { - const results: any[] = []; - let playlist: any; - for (const element of playlists) { - if (element.url && element._id) { - playlist = await this.fetchPlaylistByUrl({ - id: element._id, - title: element.title || '', - url: element.url, - }); - results.push(playlist); - } else if (element.filePath && element._id) { - playlist = await this.fetchPlaylistByFilePath({ - id: element._id, - title: element.title || '', - filePath: element.filePath, - }); - results.push(playlist); - } - } - event.sender.send( - AUTO_UPDATE_PLAYLISTS_RESPONSE, - results.filter((item) => item !== undefined) - ); - } - ) - .on(MIGRATE_PLAYLISTS, (event) => { - this.getAllPlaylists().then((playlists) => { - event.sender.send(MIGRATE_PLAYLISTS_RESPONSE, { - payload: playlists, - }); - }); - }) - .on(DELETE_ALL_PLAYLISTS, (event) => { - this.removeAllPlaylists(event); - }) - // eslint-disable-next-line @typescript-eslint/no-misused-promises - .on(OPEN_MPV_PLAYER, async (event, { url }) => { - try { - if (this.mpv === null) { - this.mpv = this.createMpvInstance(); - } - if (this.mpv.isRunning()) { - await this.mpv.load(url); - } else { - await this.mpv.start(); - await this.mpv.load(url); - } - } catch (error) { - console.log(error); - event.sender.send(ERROR, { - message: `Error: ${ - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - error?.verbose ?? - 'Something went wrong. Make sure that mpv player is installed on your system.' - } `, - }); - } - }) - .on(SET_MPV_PLAYER_PATH, (_event, mpvPlayerPath) => { - console.log('... setting mpv player path', mpvPlayerPath); - store.set(MPV_PLAYER_PATH, mpvPlayerPath); - - // recreate mpv player instance with new binary path if it was changed - if (store.get(MPV_PLAYER_PATH, mpvPlayerPath) !== mpvPlayerPath) - this.mpv = this.createMpvInstance(); - }) - .on(OPEN_VLC_PLAYER, (event, { url }) => { - const proc = child_process.spawn( - this.getVlcPath(), - [`"${url as string}"`], - { - shell: true, - } - ); - - proc.on('exit', (code) => { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - console.log(`VLC exited with code ${code}`); - }); - }) - .on(SET_VLC_PLAYER_PATH, (_event, vlcPlayerPath) => { - console.log('... setting vlc player path', vlcPlayerPath); - store.set(VLC_PLAYER_PATH, vlcPlayerPath); - }) - .on(SETTINGS_UPDATE, (_event, arg) => { - this.settings = arg; - this.server.updateSettings(); - }); - - // listeners for EPG events - ipcMain - .on(EPG_GET_PROGRAM, (_event, arg) => - this.workerWindow.webContents.send(EPG_GET_PROGRAM, arg) - ) - .on(EPG_GET_CHANNELS, (_event, arg) => - this.workerWindow.webContents.send(EPG_GET_CHANNELS, arg) - ) - .on(EPG_GET_CHANNELS_DONE, (_event, arg) => - this.mainWindow.webContents.send(EPG_GET_CHANNELS_DONE, arg) - ) - .on(EPG_GET_PROGRAM_DONE, (_event, arg) => { - this.mainWindow.webContents.send(EPG_GET_PROGRAM_DONE, arg); - }) - .on(EPG_FETCH, (_event, arg) => - this.workerWindow.webContents.send(EPG_FETCH, arg?.url) - ) - .on(EPG_FETCH_DONE, (_event, arg) => - this.mainWindow.webContents.send(EPG_FETCH_DONE, arg) - ) - .on(EPG_ERROR, (_event, arg) => - this.mainWindow.webContents.send(EPG_ERROR, arg) - ) - .on(EPG_GET_CHANNELS_BY_RANGE, (_event, arg) => { - this.workerWindow.webContents.send( - EPG_GET_CHANNELS_BY_RANGE, - arg - ); - }) - .on(EPG_GET_CHANNELS_BY_RANGE_RESPONSE, (_event, arg) => - this.mainWindow.webContents.send( - EPG_GET_CHANNELS_BY_RANGE_RESPONSE, - arg - ) - ) - .on(EPG_FORCE_FETCH, (_event, arg) => - this.workerWindow.webContents.send(EPG_FORCE_FETCH, arg) - ); - - ipcMain - .on( - XTREAM_REQUEST, - ( - event, - arg: { url: string; params: Record } - ) => { - const xtreamApiPath = '/player_api.php'; - - axios - .get(arg.url + xtreamApiPath, { - params: arg.params ?? {}, - }) - .then((result) => { - event.sender.send(XTREAM_RESPONSE, { - payload: result.data, - action: arg.params.action, - }); - }) - .catch((err) => { - event.sender.send(ERROR, { - message: - err.response?.statusText ?? - 'Error: not found', - status: err.response?.status ?? 404, - }); - }); - } - ) - .on(STALKER_REQUEST, (event, arg: any) => { - axios - .get(arg.url, { - params: arg.params ?? {}, - headers: { - Cookie: `mac=${arg.macAddress as string}`, - ...(arg.params.token - ? { - Authorization: `Bearer ${ - arg.params.token as string - }`, - } - : {}), - }, - }) - .then((result) => { - event.sender.send(STALKER_RESPONSE, { - payload: result.data, - action: arg.params.action, - }); - }) - .catch((err) => { - event.sender.send(ERROR, { - message: - err.response?.statusText ?? 'Error: not found', - status: err.response?.status ?? 404, - }); - }); - }); - } - - createMpvInstance() { - const mpvPlayerPath = this.store.get(MPV_PLAYER_PATH); - console.log('... getting mpv player path', mpvPlayerPath); - return new mpvAPI( - { ...(mpvPlayerPath ? { binary: mpvPlayerPath } : {}) }, - ['--autofit=70%'] - ); - } - - /** - * Sets the user agent header for all http requests - * @param userAgent user agent to use - * @param referer referer to use - */ - setUserAgent(userAgent: string, referer?: string): void { - if (userAgent === undefined || userAgent === null || userAgent === '') { - userAgent = this.defaultUserAgent; - } - - // Remove trailing slash from referer if it exists - let originURL: string; - if (referer?.endsWith('/')) { - originURL = referer.slice(0, -1); - } - - session.defaultSession.webRequest.onBeforeSendHeaders( - (details, callback) => { - details.requestHeaders['User-Agent'] = userAgent; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - details.requestHeaders['Referer'] = referer as string; - details.requestHeaders['Origin'] = originURL as string; - callback({ requestHeaders: details.requestHeaders }); - } - ); - console.log(`Success: Set "${userAgent}" as user agent header`); - } - - /** - * Sets epg browser window - * @param workerWindow - */ - setEpgWorkerWindow(workerWindow: BrowserWindow): void { - this.workerWindow = workerWindow; - - // store default user agent as fallback - this.defaultUserAgent = this.workerWindow.webContents.getUserAgent(); - } - - /** - * Sets browser window of the main app window - * @param mainWindow - */ - setMainWindow(mainWindow: BrowserWindow): void { - this.mainWindow = mainWindow; - } - - /** - * Converts the fetched playlist string to the playlist object, updates it in the database and sends the updated playlists array back to the renderer - * @param id id of the playlist to update - * @param playlistString updated playlist as string - */ - getRefreshedPlaylist( - args: { id: string; title: string; filePath?: string; url?: string }, - playlistString: string - ) { - const parsedPlaylist: ParsedPlaylist = - this.parsePlaylist(playlistString); - const playlist = createPlaylistObject( - args.title, - parsedPlaylist, - args.url ? args.url : args.filePath, - args.url ? 'URL' : 'FILE' - ); - return { - ...playlist, - _id: args.id, - }; - } - - sendPlaylistRefreshResponse( - playlistId: string, - playlist: Playlist, - event: Electron.IpcMainEvent - ) { - event.sender.send(PLAYLIST_UPDATE_RESPONSE, { - message: `Success! The playlist was successfully updated (${ - (playlist.playlist.items as Channel[]).length - } channels)`, - playlist: { - ...playlist, - _id: playlistId, - }, - }); - } - - /** - * Fetches the playlist from the given url and triggers the update operation - * @param id id of the playlist to update - * @param playlistString updated playlist as string - * @param event ipc event to send the response back to the renderer - */ - async fetchPlaylistByUrl( - args: { id: string; title: string; url?: string }, - event?: Electron.IpcMainEvent - ) { - if (!args.url) return; - try { - const result = await axios.get(args.url, { httpsAgent: agent }); - - const refreshedPlaylist = this.getRefreshedPlaylist( - args, - result.data - ); - if (event) { - this.sendPlaylistRefreshResponse( - refreshedPlaylist._id, - refreshedPlaylist, - event - ); - } else { - return refreshedPlaylist; - } - } catch (err) { - if (event) - event.sender.send(ERROR, { - message: `File not found. Please check the entered playlist URL again.`, - status: err.response?.status, - }); - } - } - - /** - * Fetches the playlist from the given path from the file system and triggers the update operation - * @param id id of the playlist to update - * @param playlistString updated playlist as string - * @param event ipc event to send the response back to the renderer - */ - async fetchPlaylistByFilePath( - args: { id: string; title: string; filePath?: string }, - event?: Electron.IpcMainEvent - ) { - if (!args.filePath) return; - let refreshedPlaylist: Playlist; - - try { - const playlist = await fsPromises.readFile(args.filePath, 'utf-8'); - refreshedPlaylist = this.getRefreshedPlaylist(args, playlist); - - if (event) { - this.sendPlaylistRefreshResponse( - refreshedPlaylist._id, - refreshedPlaylist, - event - ); - } else { - return refreshedPlaylist; - } - } catch (err) { - return; - } - } - - /** Sends an error message to the renderer process */ - handleFileNotFoundError( - error: { - errno: string; - code: string; - syscall: string; - path: string; - }, - event?: Electron.IpcMainEvent - ): void { - console.error(error); - if (event) { - event.sender.send(ERROR, { - message: `Sorry, playlist was not found (${error.path})`, - status: 'ENOENT', - }); - } - } - - /** - * Parses string based array to playlist object - * @param m3uString m3u playlist as string - */ - parsePlaylist(m3uString: string): ParsedPlaylist { - return parse(m3uString); - } - - getDefaultVlcPath() { - if (process.platform === 'win32') { - return path.join( - 'C:', - 'Program Files (x86)', - 'VideoLAN', - 'VLC', - 'vlc.exe' - ); - } else if (process.platform === 'linux') { - return '/usr/bin/vlc'; - } else if (process.platform === 'darwin') { - return '/Applications/VLC.app/Contents/MacOS/VLC'; - } - } - - getVlcPath() { - const customVlcPath = this.store.get(VLC_PLAYER_PATH); - if (customVlcPath) { - return customVlcPath; - } else { - return this.getDefaultVlcPath(); - } - } - - /** @deprecated - used only for migration */ - getAllPlaylists() { - return db - .find({ type: { $exists: false } }) - .sort({ position: 1, importDate: -1 }); - } - - /** @deprecated - used only for migration */ - async removeAllPlaylists(event: Electron.IpcMainEvent) { - const removeCount = await db.remove({}, { multi: true }); - console.info(removeCount, ' playlists were removed'); - fs.unlink(dbPath, (err) => { - if (err && err.code == 'ENOENT') { - console.info("File doesn't exist, won't remove it."); - } else if (err) { - console.error('Error occurred while trying to remove file'); - } else { - console.info(`${dbPath} was deleted`); - event.sender.send(IS_PLAYLISTS_MIGRATION_POSSIBLE_RESPONSE, { - result: false, - }); - } - }); - } -} diff --git a/electron/epg-worker.html b/electron/epg-worker.html deleted file mode 100644 index e6fa93f7e..000000000 --- a/electron/epg-worker.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - Hidden EPG worker - - - - - diff --git a/electron/epg-worker.ts b/electron/epg-worker.ts deleted file mode 100644 index da6b17545..000000000 --- a/electron/epg-worker.ts +++ /dev/null @@ -1,175 +0,0 @@ -const electron = require('electron'); -const ipcRenderer = electron.ipcRenderer; -const zlib = require('zlib'); -const parser = require('epg-parser'); -const axios = require('axios'); -import { - EPG_ERROR, - EPG_FETCH, - EPG_FETCH_DONE, - EPG_FORCE_FETCH, - EPG_GET_CHANNELS, - EPG_GET_CHANNELS_BY_RANGE, - EPG_GET_CHANNELS_BY_RANGE_RESPONSE, - EPG_GET_CHANNELS_DONE, - EPG_GET_PROGRAM, - EPG_GET_PROGRAM_DONE, -} from '../shared/ipc-commands'; -import { EpgChannel } from '../src/app/player/models/epg-channel.model'; -import { EpgProgram } from '../src/app/player/models/epg-program.model'; - -// EPG data store -let EPG_DATA: { channels: EpgChannel[]; programs: EpgProgram[] } = { - channels: [], - programs: [], -}; -let EPG_DATA_MERGED: { - [id: string]: EpgChannel & { programs: EpgProgram[] }; -} = {}; -const loggerLabel = '[EPG Worker]'; - -/** List with fetched EPG URLs */ -const fetchedUrls: string[] = []; - -/** - * Fetches the epg data from the given url - * @param epgUrl url of the epg file - */ -const fetchEpgDataFromUrl = (epgUrl: string) => { - try { - let axiosConfig = {}; - if (epgUrl.endsWith('.gz')) { - axiosConfig = { - responseType: 'arraybuffer', - }; - } - axios - .get(epgUrl.trim(), axiosConfig) - .then((response) => { - console.log(loggerLabel, 'url content was fetched...'); - const { data } = response; - if (epgUrl.endsWith('.gz')) { - console.log(loggerLabel, 'start unzipping...'); - zlib.gunzip(data, (_err, output) => { - parseAndSetEpg(output); - }); - } else { - parseAndSetEpg(data); - } - }) - .catch((err) => { - console.log(loggerLabel, err); - ipcRenderer.send(EPG_ERROR); - }); - } catch (error) { - console.log(loggerLabel, error); - ipcRenderer.send(EPG_ERROR); - } -}; - -/** - * Parses and sets the epg data - * @param xmlString xml file content from the fetched url as string - */ -const parseAndSetEpg = (xmlString) => { - console.log(loggerLabel, 'start parsing...'); - const parsedEpg = parser.parse(xmlString.toString()); - EPG_DATA = { - channels: [...EPG_DATA.channels, ...parsedEpg.channels], - programs: [...EPG_DATA.programs, ...parsedEpg.programs], - }; - // map programs to channels - EPG_DATA_MERGED = convertEpgData(); - ipcRenderer.send(EPG_FETCH_DONE); - console.log(loggerLabel, 'done, parsing was finished...'); -}; - -const convertEpgData = () => { - const result: { - [id: string]: EpgChannel & { programs: EpgProgram[] }; - } = {}; - - EPG_DATA?.programs?.forEach((program) => { - if (!result[program.channel]) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - const channel = EPG_DATA?.channels?.find( - (channel) => channel.id === program.channel - ) as EpgChannel; - result[program.channel] = { - ...channel, - programs: [program], - }; - } else { - result[program.channel] = { - ...result[program.channel], - programs: [...result[program.channel].programs, program], - }; - } - }); - return result; -}; - -// fetches epg data from the provided URL -ipcRenderer.on(EPG_FETCH, (event, epgUrl: string) => { - console.log(loggerLabel, 'epg fetch command was triggered'); - if (fetchedUrls.indexOf(epgUrl) > -1) { - ipcRenderer.send(EPG_FETCH_DONE); - return; - } - fetchedUrls.push(epgUrl); - fetchEpgDataFromUrl(epgUrl); -}); - -// returns the epg data for the provided channel name and date -ipcRenderer.on(EPG_GET_PROGRAM, (event, args) => { - const channelName = args.channel?.name; - const tvgId = args.channel?.tvg?.id; - if (!EPG_DATA || !EPG_DATA.channels) return; - const foundChannel = EPG_DATA?.channels?.find((epgChannel) => { - if (tvgId && tvgId === epgChannel.id) { - return epgChannel; - } else if ( - epgChannel.name.find((nameObj) => { - if ( - nameObj.value && - nameObj.value.trim() === channelName.trim() - ) - return nameObj; - }) - ) { - return epgChannel; - } - }); - - if (foundChannel) { - const programs = EPG_DATA?.programs?.filter( - (ch) => ch.channel === foundChannel.id - ); - ipcRenderer.send(EPG_GET_PROGRAM_DONE, { - payload: { channel: foundChannel, items: programs }, - }); - } else { - console.log('EPG program for the channel was not found...'); - ipcRenderer.send(EPG_GET_PROGRAM_DONE, { - payload: { channel: {}, items: [] }, - }); - } -}); - -ipcRenderer.on(EPG_GET_CHANNELS, () => { - ipcRenderer.send(EPG_GET_CHANNELS_DONE, { - payload: EPG_DATA, - }); -}); - -ipcRenderer.on(EPG_GET_CHANNELS_BY_RANGE, (event, args) => { - ipcRenderer.send(EPG_GET_CHANNELS_BY_RANGE_RESPONSE, { - payload: Object.entries(EPG_DATA_MERGED) - .slice(args.skip, args.limit) - .map((entry) => entry[1]), - }); -}); - -ipcRenderer.on(EPG_FORCE_FETCH, (event, url: string) => { - fetchEpgDataFromUrl(url); -}); diff --git a/electron/main.ts b/electron/main.ts deleted file mode 100644 index aa679de10..000000000 --- a/electron/main.ts +++ /dev/null @@ -1,154 +0,0 @@ -/* eslint-disable no-useless-catch */ -import { app, BrowserWindow, Menu } from 'electron'; -import * as path from 'path'; -import * as url from 'url'; -import { Api } from './api'; -import { AppMenu } from './menu'; - -const fixPath = require('fix-path'); -const { - setupTitlebar, - attachTitlebarToWindow, -} = require('custom-electron-titlebar/main'); -const contextMenu = require('electron-context-menu'); -const Store = require('electron-store'); -const store = new Store(); - -const WINDOW_BOUNDS = 'WINDOW_BOUNDS'; - -fixPath(); -setupTitlebar(); -let win: BrowserWindow | null = null; -const args = process.argv.slice(1), - serve = args.some((val) => val === '--serve'); - -const api = new Api(store); -contextMenu(); -require('@electron/remote/main').initialize(); - -function createWindow(): BrowserWindow { - // Create the browser window. - win = new BrowserWindow({ - width: 1000, - height: 800, - webPreferences: { - preload: path.join(__dirname, 'preload.js'), - nodeIntegration: true, - allowRunningInsecureContent: serve ? true : false, - contextIsolation: false, - webSecurity: false, - }, - resizable: true, - darkTheme: true, - icon: path.join(__dirname, '../dist/assets/icons/icon.png'), - titleBarStyle: 'hidden', - frame: false, - minWidth: 400, - minHeight: 500, - title: 'IPTVnator', - ...store.get(WINDOW_BOUNDS), - }); - attachTitlebarToWindow(win); - - require('@electron/remote/main').enable(win.webContents); - - if (serve) { - win.on('ready-to-show', () => win?.webContents.openDevTools()); - win.loadURL('http://localhost:4200'); - } else { - win.loadURL( - url.format({ - pathname: path.join(__dirname, '../dist/index.html'), - protocol: 'file:', - slashes: true, - }) - ); - } - - win.on('close', () => { - if (win) store.set(WINDOW_BOUNDS, win.getNormalBounds()); - }); - - // Emitted when the window is closed. - win.on('closed', () => { - // Dereference the window object, usually you would store window - // in an array if your app supports multi windows, this is the time - // when you should delete the corresponding element. - win = null; - - if (process.platform !== 'darwin') { - app.quit(); - } - }); - - return win; -} - -/** - * Creates hidden window for EPG worker - * Hidden window is used as an additional thread to avoid blocking of the UI by long operations - */ -function createEpgWorkerWindow() { - const window = new BrowserWindow({ - show: false, - webPreferences: { - nodeIntegration: true, - contextIsolation: false, - }, - }); - - window.loadFile('./electron/epg-worker.html'); - if (serve) { - window.webContents.openDevTools(); - } - - window.once('ready-to-show', () => { - api.setEpgWorkerWindow(window); - }); - - return window; -} - -try { - // This method will be called when Electron has finished - // initialization and is ready to create browser windows. - // Some APIs can only be used after this event occurs. - // Added 400 ms to fix the black background issue while using transparent window. More details at https://github.com/electron/electron/issues/15947 - app.on('ready', () => { - // create main window and set menu - const win = createWindow(); - const menu = new AppMenu(win); - Menu.setApplicationMenu(menu.getMenu()); - api.setMainWindow(win); - - // create hidden window for epg worker - createEpgWorkerWindow(); - }); - - // Quit when all windows are closed. - app.on('window-all-closed', () => { - // On OS X it is common for applications and their menu bar - // to stay active until the user quits explicitly with Cmd + Q - if (process.platform !== 'darwin') { - app.quit(); - } - }); - - app.on('activate', () => { - // On OS X it's common to re-create a window in the app when the - // dock icon is clicked and there are no other windows open. - if (win === null) { - win = createWindow(); - const menu = new AppMenu(win); - Menu.setApplicationMenu(menu.getMenu()); - api.setMainWindow(win); - } - }); - - app.on('before-quit', () => { - if (win) store.set(WINDOW_BOUNDS, win.getNormalBounds()); - }); -} catch (e) { - // Catch Error - throw e; -} diff --git a/electron/menu.ts b/electron/menu.ts deleted file mode 100644 index 74d0f2918..000000000 --- a/electron/menu.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { app, BrowserWindow, Menu, MenuItem, shell } from 'electron'; -import * as path from 'path'; -import { - SHOW_WHATS_NEW, - VIEW_ADD_PLAYLIST, - VIEW_SETTINGS, -} from '../shared/ipc-commands'; -const openAboutWindow = require('about-window').default; - -export class AppMenu { - /** Application menu */ - menu: Menu = new Menu(); - - /** Application window */ - window: BrowserWindow; - - constructor(appWindow: BrowserWindow) { - this.window = appWindow; - this.initMenu(); - } - - /** - * Creates context menu - * @param win browser window object - */ - initMenu(): void { - this.menu.append(this.getFileMenu()); - - // copy-paste shortcuts workaround for mac os - if (process.platform === 'darwin') { - this.menu.append(this.getEditMenu()); - } - - this.menu.append(this.getHelpMenu()); - } - - /** - * Return the application menu - */ - getMenu(): Menu { - return this.menu; - } - - /** - * Creates and returns the file menu - * @param win application window - */ - private getFileMenu(): MenuItem { - return new MenuItem({ - label: 'File', - submenu: [ - { - label: 'Add playlist', - click: () => { - if (!this.window.isDestroyed()) - this.window.webContents.send(VIEW_ADD_PLAYLIST); - }, - }, - { - type: 'separator', - }, - { - label: 'Settings', - click: () => { - if (!this.window.isDestroyed()) - this.window.webContents.send(VIEW_SETTINGS); - }, - }, - { - type: 'separator', - }, - { - label: 'Exit', - click: () => app.quit(), - accelerator: 'CmdOrCtrl+Q', - }, - ], - }); - } - - /** - * Creates and returns the edit menu - * @param win application window - */ - private getHelpMenu(): MenuItem { - return new MenuItem({ - label: 'Help', - submenu: [ - { - label: 'What is new', - click: () => { - if (!this.window.isDestroyed()) - this.window.webContents.send(SHOW_WHATS_NEW); - }, - }, - { - label: 'Report a bug', - click: () => { - shell.openExternal( - 'https://github.com/4gray/iptvnator' - ); - }, - }, - { - label: 'Buy me a coffee', - click: () => { - shell.openExternal('https://github.com/sponsors/4gray'); - }, - }, - { - label: 'Open DevTools', - click: () => { - if (!this.window.isDestroyed()) - this.window.webContents.openDevTools(); - }, - }, - { - type: 'separator', - }, - { - label: 'About', - click: () => - openAboutWindow({ - icon_path: path.join( - __dirname, - '../dist/assets/icons/icon.png' - ), - copyright: 'Copyright (c) 2020-2023 4gray', - package_json_dir: path.join(__dirname, '../'), - }), - }, - ], - }); - } - - /** - * Creates and returns the help menu - * @param win application window - */ - private getEditMenu(): MenuItem { - return new MenuItem({ - label: 'Edit', - submenu: [ - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' }, - { role: 'delete' }, - ], - }); - } -} diff --git a/electron/preload.ts b/electron/preload.ts deleted file mode 100644 index 5bd437d80..000000000 --- a/electron/preload.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Titlebar, TitlebarColor } from 'custom-electron-titlebar'; - -window.addEventListener('DOMContentLoaded', () => { - new Titlebar({ - backgroundColor: TitlebarColor.fromHex('#1b1c1c'), - }); -}); diff --git a/electron/remote-control/index.sqrl b/electron/remote-control/index.sqrl deleted file mode 100644 index 233f4ce3e..000000000 --- a/electron/remote-control/index.sqrl +++ /dev/null @@ -1,85 +0,0 @@ - - - {{it.title}} - - - - - -
-

{{it.headerText}}

-
- -
- - -
- - - - diff --git a/electron/server.ts b/electron/server.ts deleted file mode 100644 index bc603e700..000000000 --- a/electron/server.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { IncomingMessage, ServerResponse } from "http"; -import { REMOTE_CONTROL_CHANGE_CHANNEL } from "../shared/ipc-commands"; - -const http = require('http'); -const Sqrl = require('squirrelly'); -const fs = require('fs'); -const path = require('path'); - -export class Server { - /** Server instance */ - server: any; - /** Api instance */ - api: any; - - /** - * Creates a new server instance - * @param port port number - * @param api application instance - */ - constructor(_api: any) { - this.api = _api; - this.server = http.createServer(this.requestListener.bind(this)); - } - - updateSettings() { - if (this.api.settings.remoteControl && !this.server.listening) { - this.server.listen(this.api.settings.remoteControlPort); - } else if (this.server.listening && !this.api.settings.remoteControl) { - this.server.close(); - } - } - - async getTranslation() { - const language = this.api.settings && this.api.settings.language ? this.api.settings.language : 'en'; - const languageFileContent = await fs.promises.readFile(path.join(__dirname, `../src/assets/i18n/${language}.json`), 'utf-8'); - return JSON.parse(languageFileContent); - } - - getTranslationValue(key: string, translation: any, defaultValue: string) { - if (translation && translation.REMOTE_CONTROL) { - const translationValue = translation.REMOTE_CONTROL[key]; - if (translationValue) { - return translationValue; - } - } - return defaultValue; - } - - async requestListener(request: IncomingMessage, response: ServerResponse) { - const currentPage = request.url?.split('?')[0]?.split('/').splice(-1)[0]; - const translation = await this.getTranslation(); - switch (currentPage) { - case '': { - const indexFile = await fs.promises.readFile(path.join(__dirname, './remote-control/', 'index.sqrl'), 'utf-8'); - const renderedHtml = Sqrl.render(indexFile, { - headerText: this.getTranslationValue('HEADER', translation, 'Remote Control'), - upChannelText: this.getTranslationValue('UP_CHANNEL', translation, 'Up Channel'), - downChannelText: this.getTranslationValue('DOWN_CHANNEL', translation, 'Down Channel'), - upChannelUrl: '/upChannel', - downChannelUrl: '/downChannel', - title: this.getTranslationValue('TITLE', translation, 'IPTVNator'), - footer: this.getTranslationValue('FOOTER', translation, 'IPTVNator') - }); - response.writeHead(200, { 'Content-Type': 'text/html' }); - response.end(renderedHtml); - break; - } - case 'upChannel': { - this.api.mainWindow.webContents.send(REMOTE_CONTROL_CHANGE_CHANNEL, { type: 'up' }); - response.writeHead(200, { 'Content-Type': 'text/html' }); - response.end('Up Channel'); - break; - } - case 'downChannel': { - this.api.mainWindow.webContents.send(REMOTE_CONTROL_CHANGE_CHANNEL, { type: 'down' }); - response.writeHead(200, { 'Content-Type': 'text/html' }); - response.end('Down Channel'); - break; - } - default: - response.writeHead(404, { 'Content-Type': 'text/html' }); - response.end('404'); - break; - } - } -} \ No newline at end of file