diff --git a/.gitignore b/.gitignore
index b628b8f..ca0f4d4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
# Dependencies
node_modules/
+**/node_modules/
.pnp
.pnp.js
diff --git a/electron/main.ts b/electron/main.ts
index feda98c..4b26a0d 100644
--- a/electron/main.ts
+++ b/electron/main.ts
@@ -1,8 +1,15 @@
import { app, BrowserWindow, ipcMain, shell, Menu, Tray, nativeImage } from 'electron';
import * as path from 'path';
import * as url from 'url';
+import { fileURLToPath } from 'url';
+import { fork, ChildProcess } from 'child_process';
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
+let serverProcess: ChildProcess | null = null;
+
+// ES module compatible __dirname
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
let mainWindow: BrowserWindow | null = null;
let tray: Tray | null = null;
@@ -19,7 +26,7 @@ function createWindow() {
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
- webSecurity: !isDev,
+ webSecurity: false,
},
icon: path.join(__dirname, '../public/icon.png'),
});
@@ -29,14 +36,41 @@ function createWindow() {
mainWindow.loadURL('http://localhost:5173');
mainWindow.webContents.openDevTools();
} else {
- mainWindow.loadURL(
- url.format({
- pathname: path.join(__dirname, '../dist/index.html'),
- protocol: 'file:',
- slashes: true,
- })
- );
+ const indexPath = path.join(app.getAppPath(), 'dist', 'index.html');
+ console.log('Loading index.html from:', indexPath);
+
+ mainWindow.loadFile(indexPath).catch((error) => {
+ console.error('Failed to load index.html:', error);
+ // Show error in window
+ mainWindow?.webContents.executeJavaScript(`
+ document.body.innerHTML = '
+
Error Loading App
+
Failed to load index.html
+
Path: ${indexPath}
+
Error: ${error.message}
+
App Path: ${app.getAppPath()}
+
';
+ `);
+ });
}
+
+ // Log all console messages from renderer
+ mainWindow.webContents.on('console-message', (event, level, message, line, sourceId) => {
+ console.log(`[Renderer ${level}] ${message} (${sourceId}:${line})`);
+ });
+
+ // Log page load events
+ mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL) => {
+ console.error('Page failed to load:', {
+ errorCode,
+ errorDescription,
+ url: validatedURL,
+ });
+ });
+
+ mainWindow.webContents.on('did-finish-load', () => {
+ console.log('Page finished loading');
+ });
// Handle external links
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
@@ -173,8 +207,77 @@ function createTray() {
});
}
+/**
+ * Start the backend server using Electron's bundled Node.js
+ */
+function startServer() {
+ const serverDir = path.join(app.getAppPath(), 'server');
+ const serverPath = path.join(serverDir, 'src', 'index.js');
+
+ console.log('Starting server from:', serverPath);
+ console.log('Server directory:', serverDir);
+
+ // Use fork() which automatically uses Electron's bundled Node.js runtime
+ // This works in both development and packaged apps
+ serverProcess = fork(serverPath, [], {
+ cwd: serverDir,
+ env: {
+ ...process.env,
+ PORT: '3001',
+ NODE_ENV: isDev ? 'development' : 'production',
+ },
+ stdio: ['ignore', 'pipe', 'pipe', 'ipc'], // 'ipc' channel for fork communication
+ silent: false, // Allow server to output to console
+ });
+
+ serverProcess.stdout?.on('data', (data) => {
+ console.log(`[Server] ${data.toString()}`);
+ });
+
+ serverProcess.stderr?.on('data', (data) => {
+ console.error(`[Server Error] ${data.toString()}`);
+ });
+
+ serverProcess.on('exit', (code, signal) => {
+ console.log(`Server process exited with code ${code}${signal ? ` (signal: ${signal})` : ''}`);
+ serverProcess = null;
+ });
+
+ serverProcess.on('error', (error) => {
+ console.error('Failed to start server:', error);
+ serverProcess = null;
+ });
+
+ // Wait a bit for server to start before creating window
+ return new Promise((resolve) => {
+ // Give server time to start
+ setTimeout(() => {
+ // Check if server is still running
+ if (serverProcess && !serverProcess.killed) {
+ console.log('Server started successfully');
+ } else {
+ console.warn('Server may not have started properly');
+ }
+ resolve();
+ }, 2000);
+ });
+}
+
+/**
+ * Stop the backend server
+ */
+function stopServer() {
+ if (serverProcess) {
+ serverProcess.kill();
+ serverProcess = null;
+ }
+}
+
// App event handlers
-app.whenReady().then(() => {
+app.whenReady().then(async () => {
+ // Start server first
+ await startServer();
+
createWindow();
createTray();
@@ -186,11 +289,16 @@ app.whenReady().then(() => {
});
app.on('window-all-closed', () => {
+ stopServer();
if (process.platform !== 'darwin') {
app.quit();
}
});
+app.on('before-quit', () => {
+ stopServer();
+});
+
// IPC handlers for renderer communication
ipcMain.handle('get-app-path', () => {
return app.getPath('userData');
@@ -208,6 +316,27 @@ ipcMain.handle('open-external', (_event, url: string) => {
shell.openExternal(url);
});
+// Window control handlers
+ipcMain.handle('window-minimize', () => {
+ mainWindow?.minimize();
+});
+
+ipcMain.handle('window-maximize', () => {
+ if (mainWindow?.isMaximized()) {
+ mainWindow.unmaximize();
+ } else {
+ mainWindow?.maximize();
+ }
+});
+
+ipcMain.handle('window-close', () => {
+ mainWindow?.close();
+});
+
+ipcMain.handle('window-is-maximized', () => {
+ return mainWindow?.isMaximized() ?? false;
+});
+
// Handle certificate errors for development
if (isDev) {
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
diff --git a/electron/preload.ts b/electron/preload.ts
index 8de4867..504774e 100644
--- a/electron/preload.ts
+++ b/electron/preload.ts
@@ -11,6 +11,12 @@ contextBridge.exposeInMainWorld('electron', {
showItemInFolder: (path: string) => ipcRenderer.invoke('show-item-in-folder', path),
openExternal: (url: string) => ipcRenderer.invoke('open-external', url),
+ // Window controls
+ minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
+ maximizeWindow: () => ipcRenderer.invoke('window-maximize'),
+ closeWindow: () => ipcRenderer.invoke('window-close'),
+ isMaximized: () => ipcRenderer.invoke('window-is-maximized'),
+
// Navigation
onNavigate: (callback: (path: string) => void) => {
ipcRenderer.on('navigate', (_event, path) => callback(path));
@@ -29,6 +35,10 @@ declare global {
getDownloadsPath: () => Promise;
showItemInFolder: (path: string) => Promise;
openExternal: (url: string) => Promise;
+ minimizeWindow: () => Promise;
+ maximizeWindow: () => Promise;
+ closeWindow: () => Promise;
+ isMaximized: () => Promise;
onNavigate: (callback: (path: string) => void) => void;
platform: NodeJS.Platform;
isElectron: boolean;
diff --git a/electron/tsconfig.json b/electron/tsconfig.json
index 721df31..db16e2d 100644
--- a/electron/tsconfig.json
+++ b/electron/tsconfig.json
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES2020",
- "module": "CommonJS",
+ "module": "ES2020",
"lib": ["ES2020"],
"outDir": "../dist-electron",
"rootDir": ".",
diff --git a/package.json b/package.json
index 399e9fd..5426387 100644
--- a/package.json
+++ b/package.json
@@ -2,22 +2,24 @@
"name": "bestream",
"version": "2.4.0",
"description": "Cross-platform movie streaming app with Netflix-style UI",
+ "author": "beStream",
"private": true,
"type": "module",
- "main": "electron/main.js",
+ "main": "dist-electron/main.js",
"scripts": {
"dev": "vite",
"dev:all": "concurrently \"npm run dev\" \"npm run server\"",
"server": "cd server && npm start",
"server:install": "cd server && npm install",
"build": "tsc -b && vite build",
+ "build:electron": "tsc -p electron/tsconfig.json",
"preview": "vite preview",
"lint": "eslint .",
- "electron:dev": "concurrently \"vite\" \"wait-on http://localhost:5173 && electron .\"",
- "electron:build": "vite build && electron-builder",
- "build:win": "vite build && electron-builder --win",
- "build:mac": "vite build && electron-builder --mac",
- "build:linux": "vite build && electron-builder --linux",
+ "electron:dev": "npm run build:electron && concurrently \"vite\" \"wait-on http://localhost:5173 && electron .\"",
+ "electron:build": "npm run build:electron && vite build && electron-builder",
+ "build:win": "npm run build:electron && vite build && electron-builder --win",
+ "build:mac": "npm run build:electron && vite build && electron-builder --mac",
+ "build:linux": "npm run build:electron && vite build && electron-builder --linux",
"cap:sync": "npx cap sync",
"build:android": "vite build && npx cap sync android",
"build:ios": "vite build && npx cap sync ios"
@@ -69,26 +71,25 @@
},
"files": [
"dist/**/*",
- "electron/**/*"
+ "dist-electron/**/*",
+ "server/**/*"
],
"win": {
"target": [
"nsis"
],
- "icon": "public/icon.ico"
+ "sign": null
},
"mac": {
"target": [
"dmg"
- ],
- "icon": "public/icon.icns"
+ ]
},
"linux": {
"target": [
"AppImage",
"deb"
- ],
- "icon": "public/icon.png"
+ ]
},
"nsis": {
"oneClick": false,
diff --git a/public/icon.png b/public/icon.png
index 22165cd..c1ab738 100644
Binary files a/public/icon.png and b/public/icon.png differ
diff --git a/server/src/index.js b/server/src/index.js
index 200702f..1eb0825 100644
--- a/server/src/index.js
+++ b/server/src/index.js
@@ -10,7 +10,7 @@ const PORT = process.env.PORT || 3001;
// Middleware
app.use(cors({
- origin: ['http://localhost:5173', 'http://localhost:3000'],
+ origin: true, // Allow all origins for Electron app
credentials: true,
}));
app.use(express.json());
diff --git a/server/src/services/torrentManager.js b/server/src/services/torrentManager.js
index 30edd4d..1ae730e 100644
--- a/server/src/services/torrentManager.js
+++ b/server/src/services/torrentManager.js
@@ -22,7 +22,7 @@ class TorrentManager {
});
this.sessions = new Map();
this.downloadPath = path.join(os.tmpdir(), 'bestream-cache');
-
+
// Ensure download directory exists
if (!fs.existsSync(this.downloadPath)) {
fs.mkdirSync(this.downloadPath, { recursive: true });
@@ -58,7 +58,7 @@ class TorrentManager {
const existingSession = Array.from(this.sessions.values()).find(
(s) => s.hash === torrentHash && (s.status === 'ready' || s.status === 'downloading')
);
-
+
if (existingSession) {
console.log(`♻️ Reusing existing session for ${movieName}`);
// Create a new session entry that shares the same torrent
@@ -129,10 +129,10 @@ class TorrentManager {
// Helper function to setup torrent and resolve
const setupTorrent = (torrent) => {
clearTimeout(timeout);
-
+
session.torrent = torrent;
session.total = torrent.length || 0;
-
+
// Wait for torrent to be ready with metadata
const onReady = () => {
// Find the video file
@@ -159,7 +159,7 @@ class TorrentManager {
session.videoFile = videoFile;
session.status = 'downloading';
session.total = torrent.length;
-
+
console.log(`📹 Found video: ${videoFile.name} (${this.formatBytes(videoFile.length)})`);
// Select only the video file
@@ -171,7 +171,7 @@ class TorrentManager {
// Prioritize first pieces for faster playback start
videoFile.select();
-
+
// Progress updates
const onDownload = () => {
session.progress = torrent.progress;
@@ -179,12 +179,12 @@ class TorrentManager {
session.uploadSpeed = torrent.uploadSpeed;
session.peers = torrent.numPeers;
session.downloaded = torrent.downloaded;
-
+
if (onProgress) {
onProgress(session);
}
};
-
+
// Remove any existing listeners to prevent duplicates
torrent.removeAllListeners('download');
torrent.on('download', onDownload);
@@ -212,22 +212,48 @@ class TorrentManager {
// If torrent already has files (was already added), use immediately
if (torrent.files && torrent.files.length > 0) {
onReady();
+ } else if (typeof torrent.once === 'function') {
+ // Wait for metadata if torrent supports events
+ // Add timeout for metadata retrieval
+ const metadataTimeout = setTimeout(() => {
+ if (!torrent.files || torrent.files.length === 0) {
+ session.status = 'error';
+ session.error = 'Torrent metadata timeout - no peers or invalid torrent';
+ console.error(`❌ Torrent metadata timeout for: ${movieName}`);
+ reject(new Error('Torrent metadata timeout - no peers or invalid torrent'));
+ }
+ }, 30000); // 30 second timeout for metadata
+
+ torrent.once('ready', () => {
+ clearTimeout(metadataTimeout);
+ onReady();
+ });
+
+ // Also handle error events
+ torrent.once('error', (err) => {
+ clearTimeout(metadataTimeout);
+ session.status = 'error';
+ session.error = err.message || 'Torrent error';
+ console.error(`❌ Torrent error for ${movieName}:`, err.message);
+ reject(err);
+ });
} else {
- // Wait for metadata
- torrent.once('ready', onReady);
+ // Fallback: wait longer for files to appear
+ setTimeout(() => {
+ if (torrent.files && torrent.files.length > 0) {
+ onReady();
+ } else {
+ session.status = 'error';
+ session.error = 'Torrent failed to initialize - no metadata received';
+ console.error(`❌ Torrent failed to initialize: ${movieName}`);
+ reject(new Error('Torrent failed to initialize - no metadata received'));
+ }
+ }, 10000); // Increased to 10 seconds
}
};
try {
- // Check if torrent already exists in client
- const existingTorrent = this.client.get(torrentHash);
- if (existingTorrent) {
- console.log(`♻️ Reusing existing torrent: ${movieName}`);
- setupTorrent(existingTorrent);
- return;
- }
-
- this.client.add(magnetUri, {
+ this.client.add(magnetUri, {
path: this.downloadPath,
// Prioritize first and last pieces for faster start
strategy: 'sequential',
@@ -246,7 +272,7 @@ class TorrentManager {
*/
findVideoFile(torrent) {
const videoExtensions = ['.mp4', '.mkv', '.avi', '.mov', '.webm', '.m4v'];
-
+
// Sort by size and find largest video file
const videoFiles = torrent.files
.filter((file) => {
@@ -319,7 +345,19 @@ class TorrentManager {
async stopSession(sessionId) {
const session = this.sessions.get(sessionId);
if (session && session.torrent) {
- session.torrent.destroy();
+ // Remove torrent from client if it has a destroy method
+ if (typeof session.torrent.destroy === 'function') {
+ session.torrent.destroy();
+ } else {
+ // Fallback: remove from client using the hash
+ try {
+ this.client.remove(session.torrent, (err) => {
+ if (err) console.error('Error removing torrent:', err);
+ });
+ } catch (err) {
+ console.error('Error removing torrent:', err);
+ }
+ }
}
this.sessions.delete(sessionId);
}
@@ -355,5 +393,4 @@ class TorrentManager {
}
}
-export const torrentManager = new TorrentManager();
-
+export const torrentManager = new TorrentManager();
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
index ce4c73a..e080593 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -6,26 +6,16 @@ import Browse from './pages/Browse';
import Search from './pages/Search';
import MovieDetails from './pages/MovieDetails';
import Player from './pages/Player';
-import TVPlayer from './pages/TVPlayer';
import Watchlist from './pages/Watchlist';
import Downloads from './pages/Downloads';
import Settings from './pages/Settings';
import History from './pages/History';
-import TVShows from './pages/TVShows';
-import TVShowDetails from './pages/TVShowDetails';
-import Music from './pages/Music';
-import ArtistDetails from './pages/ArtistDetails';
-import AlbumDetails from './pages/AlbumDetails';
-import Calendar from './pages/Calendar';
import Queue from './pages/Queue';
import { useSettingsStore } from './stores/settingsStore';
-import { useInitializeIntegrations } from './hooks/useIntegration';
function App() {
const { settings } = useSettingsStore();
- // Initialize *arr service connections on app startup
- useInitializeIntegrations();
useEffect(() => {
// Apply theme
@@ -41,15 +31,7 @@ function App() {
} />
} />
} />
- {/* TV Shows (Sonarr) */}
- } />
- } />
- {/* Music (Lidarr) */}
- } />
- } />
- } />
- {/* Calendar & Queue */}
- } />
+ {/* Queue */}
} />
{/* User */}
} />
@@ -59,8 +41,6 @@ function App() {
{/* Full-screen player */}
} />
- {/* TV Show Player */}
- } />
);
}
diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx
index 1f6b124..1d42ee8 100644
--- a/src/components/layout/Navbar.tsx
+++ b/src/components/layout/Navbar.tsx
@@ -8,22 +8,19 @@ import {
X,
Home,
Film,
- Tv,
- Music,
- Calendar,
Heart,
- Clock,
Download,
Settings,
+ Minus,
+ Square,
+ Maximize2,
} from 'lucide-react';
import { clsx } from 'clsx';
+import { isElectron } from '../../utils/platform';
const navLinks = [
{ path: '/', label: 'Home', icon: Home },
{ path: '/browse', label: 'Movies', icon: Film },
- { path: '/tv', label: 'TV Shows', icon: Tv },
- { path: '/music', label: 'Music', icon: Music },
- { path: '/calendar', label: 'Calendar', icon: Calendar },
{ path: '/watchlist', label: 'My List', icon: Heart },
{ path: '/downloads', label: 'Downloads', icon: Download },
];
@@ -33,8 +30,10 @@ export default function Navbar() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [isSearchOpen, setIsSearchOpen] = useState(false);
+ const [isMaximized, setIsMaximized] = useState(false);
const location = useLocation();
const navigate = useNavigate();
+ const isElectronApp = isElectron();
useEffect(() => {
const handleScroll = () => {
@@ -49,6 +48,21 @@ export default function Navbar() {
setIsMobileMenuOpen(false);
}, [location.pathname]);
+ // Check if window is maximized (Electron only)
+ useEffect(() => {
+ if (isElectronApp && window.electron) {
+ const checkMaximized = async () => {
+ const maximized = await window.electron.isMaximized();
+ setIsMaximized(maximized);
+ };
+ checkMaximized();
+
+ // Check periodically (Electron doesn't have a direct event for this)
+ const interval = setInterval(checkMaximized, 500);
+ return () => clearInterval(interval);
+ }
+ }, [isElectronApp]);
+
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
if (searchQuery.trim()) {
@@ -58,8 +72,53 @@ export default function Navbar() {
}
};
+ const handleMinimize = () => {
+ if (isElectronApp && window.electron) {
+ window.electron.minimizeWindow();
+ }
+ };
+
+ const handleMaximize = () => {
+ if (isElectronApp && window.electron) {
+ window.electron.maximizeWindow();
+ setIsMaximized(!isMaximized);
+ }
+ };
+
+ const handleClose = () => {
+ if (isElectronApp && window.electron) {
+ window.electron.closeWindow();
+ }
+ };
+
return (
<>
+ {/* Window Controls (Electron only, Windows/Linux) */}
+ {isElectronApp && (window.electron?.platform === 'win32' || window.electron?.platform === 'linux') && (
+
+
+
+
+
+ )}