import os import sys import requests import time import xml.etree.ElementTree as ET from datetime import datetime, timedelta from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QListWidget, QSlider, QMessageBox, QTextEdit, QListWidgetItem, QScrollArea, QFrame, QMenuBar, QMenu, QAction, QSizePolicy, QToolBar, QStatusBar, QInputDialog, QLineEdit) from PyQt5.QtCore import Qt, QTimer, QSize, QPoint, QEvent from PyQt5.QtGui import QColor, QIcon, QFont, QPalette,QCloseEvent import vlc import subprocess class EPGDisplay(QWidget): def __init__(self, parent=None): super().__init__(parent) self.setLayout(QVBoxLayout()) self.scroll = QScrollArea() self.scroll.setWidgetResizable(True) self.layout().addWidget(self.scroll) self.content = QWidget() self.content.setLayout(QHBoxLayout()) self.content.layout().setContentsMargins(0, 0, 0, 0) self.content.layout().setSpacing(0) self.scroll.setWidget(self.content) self.setMinimumHeight(150) self.setMaximumHeight(200) def update_epg(self, programs): # Clear existing widgets for i in reversed(range(self.content.layout().count())): self.content.layout().itemAt(i).widget().deleteLater() # Add program blocks for program in programs: frame = QFrame() frame.setFrameShape(QFrame.Box) frame.setLineWidth(1) frame.setStyleSheet("background-color: #f0f0f0;") layout = QVBoxLayout() layout.setContentsMargins(5, 5, 5, 5) time_label = QLabel(program['start_time'].strftime("%H:%M")) time_label.setStyleSheet("font-weight: bold;") title_label = QLabel(program['title']) title_label.setWordWrap(True) layout.addWidget(time_label) layout.addWidget(title_label) frame.setLayout(layout) # Calculate width based on duration (1 hour = 200px) duration_hours = (program['end_time'] - program['start_time']).total_seconds() / 3600 frame.setFixedWidth(int(duration_hours * 200)) self.content.layout().addWidget(frame) class VideoWindow(QMainWindow): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("IPTV Player - Video") self.setAttribute(Qt.WA_DeleteOnClose) # Initialize variables self.is_fullscreen = False self.info_display_time = 10 # seconds self.show_info = False self.info_timer = QTimer() self.info_timer.timeout.connect(self.hide_info) self.parent = parent # Reference to parent (IPTVPlayer) self.current_channel = "" self.debug_console = parent.debug_console if parent else None # Create main widget self.video_frame = QFrame() self.video_frame.setFrameShape(QFrame.NoFrame) self.video_frame.setStyleSheet("background-color: black;") self.video_frame.mouseDoubleClickEvent = self.toggle_fullscreen self.video_frame.mousePressEvent = self.show_stream_info # Info labels self.channel_label = QLabel(self.video_frame) self.channel_label.setStyleSheet("color: white; font-size: 14px; background-color: rgba(0, 0, 0, 150);") self.channel_label.move(10, 10) self.channel_label.hide() self.clock_label = QLabel(self.video_frame) self.clock_label.setStyleSheet("color: white; font-size: 14px; background-color: rgba(0, 0, 0, 150);") self.clock_label.hide() self.resolution_label = QLabel(self.video_frame) self.resolution_label.setStyleSheet("color: white; font-size: 14px; background-color: rgba(0, 0, 0, 150);") self.resolution_label.hide() # Clock timer self.clock_timer = QTimer(self) self.clock_timer.timeout.connect(self.update_clock) self.clock_timer.start(1000) # Create toolbar self.create_toolbar() # Volume control self.volume_slider = QSlider(Qt.Horizontal) self.volume_slider.setRange(0, 100) self.volume_slider.setValue(50) self.volume_slider.valueChanged.connect(self.set_volume) self.volume_slider.setFixedWidth(150) # Layout control_layout = QHBoxLayout() control_layout.addWidget(self.toolbar) control_layout.addStretch() control_layout.addWidget(self.volume_slider) main_layout = QVBoxLayout() main_layout.addWidget(self.video_frame, 1) main_layout.addLayout(control_layout) container = QWidget() container.setLayout(main_layout) self.setCentralWidget(container) # Create menu self.create_menu() # Position window self.resize_to_quarter_screen() self.log_message("VideoWindow initialized") self.hide() self.setWindowFlags( Qt.Window | Qt.WindowSystemMenuHint | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint | Qt.WindowCloseButtonHint ) self.show() def resize_to_quarter_screen(self): """Resize window to take top right quarter of screen""" screen = QApplication.primaryScreen().availableGeometry() width = int(screen.width() * 0.25) height = int(screen.height() * 0.25) self.setGeometry( screen.left() + screen.width() - width, screen.top(), width, height ) self.log_message(f"VideoWindow resized to {width}x{height}") def log_message(self, message, error=False): """Log message to debug console""" if self.debug_console: timestamp = time.strftime("%H:%M:%S", time.localtime()) if error: self.debug_console.setTextColor(Qt.red) self.debug_console.append(f"[ERROR {timestamp}] {message}") else: self.debug_console.setTextColor(Qt.black) self.debug_console.append(f"[INFO {timestamp}] {message}") self.debug_console.setTextColor(Qt.black) self.debug_console.ensureCursorVisible() def _restore_window_flags(self): self.setWindowFlags(Qt.Window | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint | Qt.WindowCloseButtonHint) self.show() def toggle_fullscreen(self, event=None): """Toggle borderless fullscreen mode""" try: if self.is_fullscreen: self.is_fullscreen = False self.setWindowFlags(Qt.Window | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint | Qt.WindowCloseButtonHint) self.showNormal() # Ensures buttons and frame reappear self.toolbar.show() self.menuBar().show() self.resize_to_quarter_screen() self.log_message("Exited fullscreen mode") else: self.is_fullscreen = True self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint) self.showFullScreen() # Hides border and buttons self.toolbar.hide() self.menuBar().hide() self.log_message("Entered fullscreen mode") except Exception as e: self.log_message(f"Error in toggle_fullscreen: {str(e)}", error=True) def create_toolbar(self): self.toolbar = QToolBar() self.toolbar.setIconSize(QSize(24, 24)) # Play/pause button self.play_action = QAction(QIcon.fromTheme("media-playback-start"), "Play", self) self.play_action.triggered.connect(self.toggle_play_pause) self.toolbar.addAction(self.play_action) # Stop button self.stop_action = QAction(QIcon.fromTheme("media-playback-stop"), "Stop", self) self.stop_action.triggered.connect(self.stop) self.toolbar.addAction(self.stop_action) # Previous channel self.prev_action = QAction(QIcon.fromTheme("media-skip-backward"), "Previous", self) self.prev_action.triggered.connect(self.prev_channel) self.toolbar.addAction(self.prev_action) # Next channel self.next_action = QAction(QIcon.fromTheme("media-skip-forward"), "Next", self) self.next_action.triggered.connect(self.next_channel) self.toolbar.addAction(self.next_action) # Fullscreen self.fullscreen_action = QAction(QIcon.fromTheme("view-fullscreen"), "Fullscreen", self) self.fullscreen_action.triggered.connect(self.toggle_fullscreen) self.toolbar.addAction(self.fullscreen_action) def create_menu(self): menubar = self.menuBar() # File menu file_menu = menubar.addMenu("File") self.update_action = QAction("Update Playlist", self) self.update_action.triggered.connect(self.update_playlist) file_menu.addAction(self.update_action) self.login_action = QAction("Change Login", self) self.login_action.triggered.connect(self.change_login) file_menu.addAction(self.login_action) quit_action = QAction("Quit", self) quit_action.triggered.connect(self.close_all) file_menu.addAction(quit_action) # View menu view_menu = menubar.addMenu("View") self.toggle_playlist_action = QAction("Show/Hide Playlist", self, checkable=True) self.toggle_playlist_action.setChecked(True) self.toggle_playlist_action.triggered.connect(self.toggle_playlist) view_menu.addAction(self.toggle_playlist_action) self.always_on_top_action = QAction("Always on Top", self, checkable=True) self.always_on_top_action.setChecked(True) self.always_on_top_action.triggered.connect(self.toggle_always_on_top) view_menu.addAction(self.always_on_top_action) self.show_resolution_action = QAction("Show Resolution Info", self, checkable=True) self.show_resolution_action.setChecked(True) self.show_resolution_action.triggered.connect(self.toggle_resolution_info) view_menu.addAction(self.show_resolution_action) self.show_clock_action = QAction("Show Clock", self, checkable=True) self.show_clock_action.setChecked(True) self.show_clock_action.triggered.connect(self.toggle_clock_info) view_menu.addAction(self.show_clock_action) # Help menu help_menu = menubar.addMenu("Help") about_action = QAction("About", self) about_action.triggered.connect(self.show_about) help_menu.addAction(about_action) def position_window(self): """Position the video window in top right quadrant""" screen = QApplication.primaryScreen().availableGeometry() width = int(screen.width() * 0.25) height = int(screen.height() * 0.25) self.setGeometry( screen.left() + screen.width() - width, screen.top(), width, height ) def init_vlc(self): """Initialize VLC player""" try: self.instance = vlc.Instance() self.media_player = self.instance.media_player_new() if sys.platform == "win32": self.media_player.set_hwnd(self.video_frame.winId()) else: self.media_player.set_xwindow(self.video_frame.winId()) # Timer to check stream info self.stream_info_timer = QTimer() self.stream_info_timer.timeout.connect(self.update_stream_info) self.stream_info_timer.start(1000) self.log_message("VLC initialized successfully") except Exception as e: self.log_message(f"VLC initialization failed: {str(e)}", error=True) def play_stream(self, url, title): """Play a stream URL""" try: self.current_channel = title self.setWindowTitle(f"IPTV Player - {title}") self.log_message(f"Attempting to play: {title} ({url})") if hasattr(self, 'media_player'): self.media_player.stop() media = self.instance.media_new(url) media.add_option(':network-caching=3000') media.add_option(':file-caching=3000') media.add_option(':live-caching=3000') self.media_player.set_media(media) self.media_player.play() # Update play/pause button self.play_action.setIcon(QIcon.fromTheme("media-playback-pause")) self.play_action.setText("Pause") self.log_message(f"Playing stream: {title}") except Exception as e: self.log_message(f"Error playing stream: {str(e)}", error=True) def toggle_play_pause(self): """Toggle between play and pause""" if hasattr(self, 'media_player'): if self.media_player.is_playing(): self.media_player.pause() self.play_action.setIcon(QIcon.fromTheme("media-playback-start")) self.play_action.setText("Play") else: self.media_player.play() self.play_action.setIcon(QIcon.fromTheme("media-playback-pause")) self.play_action.setText("Pause") def stop(self): """Stop playback""" if hasattr(self, 'media_player'): self.media_player.stop() self.play_action.setIcon(QIcon.fromTheme("media-playback-start")) self.play_action.setText("Play") def set_volume(self, value): """Set player volume""" if hasattr(self, 'media_player'): self.media_player.audio_set_volume(value) def toggle_fullscreen(self, event=None): """Toggle borderless fullscreen mode""" if self.is_fullscreen: self.showNormal() self.toolbar.show() self.menuBar().show() self.is_fullscreen = False self.resize_to_quarter_screen() else: self.showFullScreen() self.toolbar.hide() self.menuBar().hide() self.is_fullscreen = True # Remove window frame in fullscreen self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint) self.show() def keyPressEvent(self, event): """Handle key presses""" if event.key() == Qt.Key_Escape and self.is_fullscreen: self.toggle_fullscreen() elif event.key() == Qt.Key_Space: self.toggle_play_pause() elif event.key() == Qt.Key_Left: self.prev_channel() elif event.key() == Qt.Key_Right: self.next_channel() else: super().keyPressEvent(event) def show_stream_info(self, event): """Show stream information on screen""" if not self.show_info: self.show_info = True self.update_info_labels() self.info_timer.start(self.info_display_time * 1000) def hide_info(self): """Hide stream information""" self.show_info = False self.channel_label.hide() self.clock_label.hide() self.resolution_label.hide() self.info_timer.stop() def update_info_labels(self): """Update the information labels""" if not self.show_info: return # Channel name self.channel_label.setText(f"Channel: {self.current_channel}") self.channel_label.adjustSize() self.channel_label.show() # Clock if hasattr(self, 'show_clock_action') and self.show_clock_action.isChecked(): self.update_clock() self.clock_label.show() # Resolution info if hasattr(self, 'media_player') and hasattr(self, 'show_resolution_action') and self.show_resolution_action.isChecked(): try: width = self.media_player.video_get_width() or 0 height = self.media_player.video_get_height() or 0 fps = self.media_player.get_fps() or 0 if width > 0 and height > 0: resolution = "" if width >= 3840 or height >= 2160: resolution = "4K" elif width >= 1920 or height >= 1080: resolution = "FHD" elif width >= 1280 or height >= 720: resolution = "HD" else: resolution = "SD" self.resolution_label.setText(f"{width}x{height} {resolution} {int(fps)}fps") self.resolution_label.adjustSize() # Position at bottom right self.resolution_label.move( self.video_frame.width() - self.resolution_label.width() - 10, self.video_frame.height() - self.resolution_label.height() - 10 ) self.resolution_label.show() self.log_message(f"Resolution: {width}x{height} {resolution} {int(fps)}fps") except Exception as e: self.log_message(f"Error getting resolution: {str(e)}", error=True) def update_clock(self): """Update the clock display""" current_time = time.strftime("%H:%M:%S") self.clock_label.setText(current_time) self.clock_label.adjustSize() # Position at top right self.clock_label.move( self.video_frame.width() - self.clock_label.width() - 10, 10 ) def update_stream_info(self): """Update stream information periodically""" if self.show_info: self.update_info_labels() def prev_channel(self): """Switch to previous channel""" if self.parent: current_row = self.parent.channel_list.currentRow() if current_row > 0: self.parent.channel_list.setCurrentRow(current_row - 1) self.parent.play_selected() self.log_message("Switched to previous channel") def next_channel(self): """Switch to next channel""" if self.parent: current_row = self.parent.channel_list.currentRow() if current_row < self.parent.channel_list.count() - 1: self.parent.channel_list.setCurrentRow(current_row + 1) self.parent.play_selected() self.log_message("Switched to next channel") def update_playlist(self): """Update playlist from server""" # This should trigger the playlist update in the main window pass def change_login(self): """Change login credentials""" username, ok1 = QInputDialog.getText(self, "Change Login", "Enter new username:") if ok1: password, ok2 = QInputDialog.getText(self, "Change Login", "Enter new password:", QLineEdit.Password) if ok2: # Save new credentials (implementation depends on your auth system) QMessageBox.information(self, "Success", "Login credentials updated") def toggle_playlist(self): """Toggle playlist visibility""" if self.parent: self.parent.setVisible(not self.parent.isVisible()) self.log_message(f"Playlist visibility toggled: {self.parent.isVisible()}") def toggle_always_on_top(self, state): """Toggle always on top""" if state: self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint) else: self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint) self.show() def toggle_resolution_info(self, state): """Toggle resolution info display""" pass def toggle_clock_info(self, state): """Toggle clock display""" pass def show_about(self): """Show about dialog""" about_text = """IPTV Player

Copyright © 2025 Home13TV
Contact: home13tvdevice@gmail.com for subscription and technical support.

This software is provided for authorized users only.
Unauthorized distribution or use is prohibited.""" QMessageBox.about(self, "About IPTV Player", about_text) def close_all(self): """Close both windows""" self.video_window.close() super().closeEvent(QCloseEvent()) def closeEvent(self, event): """Handle window close""" if hasattr(self, 'media_player'): self.media_player.stop() event.accept() class IPTVPlayer(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("IPTV Player - Playlist") self.setAttribute(Qt.WA_DeleteOnClose) self.php_port = 8888 self.php_dir = os.path.join(os.path.dirname(__file__), "php_files") os.makedirs(self.php_dir, exist_ok=True) self.php_process = None self.epg_data = {} self.current_epg_url = "" # Initialize UI self.init_ui() # Start PHP server self.start_php_server() # Download and process files self.download_and_process_files() self.hide() self.setWindowFlags( Qt.Window | Qt.WindowSystemMenuHint | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint | Qt.WindowCloseButtonHint ) self.setGeometry(100, 100, 800, 600) self.video_window = VideoWindow(self) self.show() self.video_window.show() # Position windows self.position_windows() # Initialize VLC in video window self.video_window.init_vlc() def init_ui(self): """Initialize the playlist window UI""" main_widget = QWidget(self) layout = QVBoxLayout() main_widget.setLayout(layout) # Channel list self.channel_list = QListWidget() ''' self.channel_list.setStyleSheet(""" QListWidget { font-size: 12px; } QListWidget::item { padding: 5px; } QListWidget::item:selected { background-color: #0078d7; color: white; } """) self.channel_list.itemDoubleClicked.connect(self.play_selected) self.channel_list.currentItemChanged.connect(self.update_epg_display) ''' layout.addWidget(self.channel_list, 3) # EPG display self.epg_display = EPGDisplay() layout.addWidget(self.epg_display, 1) # Debug console self.debug_console = QTextEdit() self.debug_console.setReadOnly(True) self.debug_console.setMaximumHeight(100) layout.addWidget(self.debug_console) # Controls control_layout = QHBoxLayout() self.play_button = QPushButton("Play") # self.play_button.clicked.connect(self.play_selected) self.stop_button = QPushButton("Stop") # self.stop_button.clicked.connect(self.stop) self.reload_button = QPushButton("Update") # self.reload_button.clicked.connect(self.download_and_process_files) control_layout.addWidget(self.play_button) control_layout.addWidget(self.stop_button) control_layout.addWidget(self.reload_button) layout.addLayout(control_layout) self.setCentralWidget(main_widget) def _restore_window_flags(self): self.setWindowFlags(Qt.Window | Qt.WindowMinimizeButtonHint | Qt.WindowMaximizeButtonHint | Qt.WindowCloseButtonHint) self.show() def position_windows(self): """Position windows while preserving decorations""" screen = QApplication.primaryScreen().availableGeometry() # 1. First position the playlist window (left half) playlist_width = int(screen.width() * 0.5) # Use move() and resize() separately instead of setGeometry() self.move(screen.left(), screen.top()) self.resize(playlist_width, screen.height()) # 2. Then position video window (top right quarter) video_width = int(screen.width() * 0.25) video_height = int(screen.height() * 0.25) # For child window, ensure it's properly shown first self.video_window.showNormal() # Ensure window is in normal state self.video_window.move( screen.left() + screen.width() - video_width, screen.top() ) self.video_window.resize(video_width, video_height) # Force window frame update self.video_window.setWindowFlags(self.video_window.windowFlags()) self.video_window.show() self.log_message("Windows positioned (safe mode)") def start_php_server(self): """Start the built-in PHP server""" try: os.chdir(self.php_dir) self.php_process = subprocess.Popen( ["php", "-S", f"localhost:{self.php_port}"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0 ) self.log_message(f"PHP server started on port {self.php_port}") except Exception as e: self.log_message(f"Failed to start PHP server: {str(e)}", error=True) QMessageBox.critical(self, "PHP Server Error", "Could not start PHP server.\n" "Please check if PHP is properly installed.") def log_message(self, message, error=False): """Add message to debug console""" timestamp = time.strftime("%H:%M:%S", time.localtime()) if error: self.debug_console.setTextColor(Qt.red) self.debug_console.append(f"[ERROR {timestamp}] {message}") else: self.debug_console.setTextColor(Qt.black) self.debug_console.append(f"[INFO {timestamp}] {message}") self.debug_console.setTextColor(Qt.black) self.debug_console.ensureCursorVisible() def download_and_process_files(self): """Download and process all required files""" files = { "myportal.php": "https://iptv.nywebforum.com/myportal.php?download=true", "mac_list.php": "https://iptv.nywebforum.com/mac_list.php?download=true", "playlist.m3u": "https://iptv.nywebforum.com/playlist.m3u" } for filename, url in files.items(): try: temp_path = os.path.join(self.php_dir, f"temp_{filename}") response = requests.get(url, timeout=10) with open(temp_path, 'wb') as f: f.write(response.content) if filename == "playlist.m3u": with open(temp_path, 'r', encoding='utf-8') as f: content = f.read() if "x-tvg-url=" in content: self.current_epg_url = content.split('x-tvg-url="')[1].split('"')[0] final_path = os.path.join(self.php_dir, filename) if os.path.exists(final_path): os.remove(final_path) os.rename(temp_path, final_path) self.log_message(f"Downloaded {filename}") except Exception as e: self.log_message(f"Error processing {filename}: {str(e)}", error=True) if os.path.exists(temp_path): os.remove(temp_path) self.process_url_replacements() self.load_playlist() if self.current_epg_url: self.load_epg() def process_url_replacements(self): """Replace all server URLs with localhost URLs""" try: playlist_path = os.path.join(self.php_dir, "playlist.m3u") if os.path.exists(playlist_path): with open(playlist_path, 'r', encoding='utf-8') as f: content = f.read() content = content.replace( "https://iptv.nywebforum.com", f"http://localhost:{self.php_port}" ) with open(playlist_path, 'w', encoding='utf-8') as f: f.write(content) self.log_message("URL replacements completed") except Exception as e: self.log_message(f"Error during URL replacement: {str(e)}", error=True) def load_epg(self): """Load EPG data from XMLTV file""" try: response = requests.get(self.current_epg_url, timeout=10) root = ET.fromstring(response.content) self.epg_data = {} for programme in root.findall('programme'): channel = programme.get('channel') start_time = datetime.strptime(programme.get('start'), '%Y%m%d%H%M%S %z') end_time = datetime.strptime(programme.get('stop'), '%Y%m%d%H%M%S %z') title = programme.find('title').text if programme.find('title') is not None else "No Title" if channel not in self.epg_data: self.epg_data[channel] = [] self.epg_data[channel].append({ 'start_time': start_time, 'end_time': end_time, 'title': title }) self.log_message(f"Loaded EPG data for {len(self.epg_data)} channels") except Exception as e: self.log_message(f"Error loading EPG: {str(e)}", error=True) def update_epg_display(self, current, previous): """Update EPG display when channel selection changes""" if current is None: return channel_id = current.data(Qt.UserRole + 1) # Channel ID for EPG lookup now = datetime.now().astimezone() end_time = now + timedelta(hours=2) programs = [] if channel_id in self.epg_data: for program in self.epg_data[channel_id]: if program['end_time'] > now and program['start_time'] < end_time: programs.append(program) self.epg_display.update_epg(programs) def load_playlist(self): """Load the playlist with channel names and icons""" playlist_path = os.path.join(self.php_dir, "playlist.m3u") if os.path.exists(playlist_path): try: with open(playlist_path, 'r', encoding='utf-8') as f: lines = f.readlines() self.channel_list.clear() current_channel = None current_id = None for line in lines: line = line.strip() if line.startswith("#EXTINF:"): parts = line.split(',') if len(parts) > 1: current_channel = parts[1].strip() if 'tvg-id="' in line: current_id = line.split('tvg-id="')[1].split('"')[0] elif line and not line.startswith("#"): if current_channel: item = QListWidgetItem(current_channel) item.setData(Qt.UserRole, line) # Store URL item.setData(Qt.UserRole + 1, current_id) # Store channel ID self.channel_list.addItem(item) current_channel = None current_id = None if self.channel_list.count() > 0: self.channel_list.setCurrentRow(0) QTimer.singleShot(1000, self.play_selected) self.log_message(f"Playlist loaded with {self.channel_list.count()} channels") except Exception as e: self.log_message(f"Error loading playlist: {str(e)}", error=True) def play_selected(self): """Play the selected channel""" selected = self.channel_list.currentItem() if not selected: return url = selected.data(Qt.UserRole) self.video_window.play_stream(url, selected.text()) def stop(self): """Stop playback""" self.video_window.stop() def closeEvent(self, event): """Clean up when closing""" self.log_message("Closing application...") if self.php_process: self.php_process.terminate() try: self.php_process.wait(timeout=1) except subprocess.TimeoutExpired: self.php_process.kill() self.video_window.close() super().closeEvent(event) if __name__ == "__main__": app = QApplication(sys.argv) # Check dependencies try: import requests import vlc except ImportError as e: QMessageBox.critical(None, "Missing Dependencies", f"Required packages missing:\n{str(e)}\n" "Please install with: pip install python-vlc requests") sys.exit(1) # Check if PHP is available try: subprocess.run(["php", "-v"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) except: QMessageBox.critical(None, "PHP Not Found", "PHP is required but not found in system PATH.\n" "Please install PHP or place PHP binaries in the application directory.") sys.exit(1) player = IPTVPlayer() player.show() sys.exit(app.exec_())