import os import sys import time import ctypes import requests import subprocess import json from PyQt5.QtWidgets import ( QApplication, QMainWindow, QVBoxLayout, QListWidget, QWidget, QHBoxLayout, QListWidgetItem, QLabel, QSplitter, QFrame ) from PyQt5.QtCore import Qt, QTimer from PyQt5.QtGui import QGuiApplication import vlc import subprocess import re class VideoFrame(QFrame): def __init__(self, parent=None): super().__init__(parent) self.setFrameStyle(QFrame.Panel | QFrame.Sunken) self.setStyleSheet("background-color: black;") self.setFocusPolicy(Qt.StrongFocus) def mouseDoubleClickEvent(self, event): if hasattr(self.parent(), "toggle_fullscreen"): self.parent().toggle_fullscreen() class VLCPlayer: def __init__(self, video_frame): self.instance = vlc.Instance() self.media_player = self.instance.media_player_new() self.video_frame = video_frame if sys.platform == "win32": self.media_player.set_hwnd(int(self.video_frame.winId())) else: self.media_player.set_xwindow(int(self.video_frame.winId())) def play_stream(self, url): if self.media_player.is_playing(): self.media_player.stop() time.sleep(0.5) media = self.instance.media_new(url) media.add_option(":network-caching=3000") self.media_player.set_media(media) self.media_player.play() def stop(self): self.media_player.stop() def release(self): self.media_player.release() self.instance.release() class IPTVWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("IPTV Player with EPG") self.setGeometry(0, 0, 1280, 720) self.php_dir = os.path.join(os.path.dirname(__file__), "php_files") self.stream_info_cache = {} self.init_ui() # Ensure PHP server is running if self.wait_for_php_server(): self.download_playlist() else: print("PHP server not ready, skipping playlist load.") def save_last_channel_index(self): try: index = self.channel_list.currentRow() with open("last_channel.txt", "w") as f: f.write(str(index)) except: pass def load_last_channel_index(self): try: with open("last_channel.txt", "r") as f: return int(f.read().strip()) except: return None def eventFilter(self, source, event): if event.type() == event.MouseMove and self.is_fullscreen: self.control_widget.setVisible(True) self.auto_hide_timer.start() return super().eventFilter(source, event) def keyPressEvent(self, event): key = event.key() if key == Qt.Key_Escape and self.is_fullscreen: self.toggle_fullscreen() elif key == Qt.Key_Space: self.toggle_play_pause() elif key == Qt.Key_Right: self.play_next_channel() elif key == Qt.Key_Left: self.play_previous_channel() elif key == Qt.Key_F: self.toggle_fullscreen() def prewarm_streams(self, skip_url): import threading def warm(): for i in range(self.channel_list.count()): item = self.channel_list.item(i) url = item.data(Qt.UserRole) if url != skip_url: try: requests.head(url, timeout=3) except: pass threading.Thread(target=warm, daemon=True).start() def wait_for_php_server(self, timeout=5): import socket start_time = time.time() while time.time() - start_time < timeout: try: with socket.create_connection(("localhost", 8888), timeout=1): return True except Exception: time.sleep(0.5) print("PHP server not responding on port 8888.") return False def toggle_fullscreen(self): if not hasattr(self, 'is_fullscreen'): self.is_fullscreen = False if not self.is_fullscreen: self.video_frame.setParent(None) self.fullscreen_container = QWidget() layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.video_frame) self.fullscreen_container.setLayout(layout) self.fullscreen_container.setWindowFlags(Qt.Window | Qt.FramelessWindowHint) self.fullscreen_container.showFullScreen() self.is_fullscreen = True else: self.fullscreen_container.close() self.splitter.widget(1).layout().insertWidget(0, self.video_frame) self.is_fullscreen = False def play_next_channel(self): current_row = self.channel_list.currentRow() if current_row < self.channel_list.count() - 1: self.channel_list.setCurrentRow(current_row + 1) self.on_channel_clicked(self.channel_list.currentItem()) def play_previous_channel(self): current_row = self.channel_list.currentRow() if current_row > 0: self.channel_list.setCurrentRow(current_row - 1) self.on_channel_clicked(self.channel_list.currentItem()) def toggle_play_pause(self): if self.player.media_player.is_playing(): self.player.media_player.pause() else: self.player.media_player.play() def toggle_fullscreen(self): if not hasattr(self, 'is_fullscreen'): self.is_fullscreen = False if not self.is_fullscreen: self.video_frame.setParent(None) self.fullscreen_container = QWidget() layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.video_frame) self.fullscreen_container.setLayout(layout) self.fullscreen_container.setWindowFlags(Qt.Window | Qt.FramelessWindowHint) self.fullscreen_container.showFullScreen() self.is_fullscreen = True else: self.fullscreen_container.close() self.splitter.widget(1).layout().insertWidget(0, self.video_frame) self.is_fullscreen = False self.original_geometry = None self.init_ui() if self.wait_for_php_server(): self.download_playlist() else: print('Skipping playlist load.') def init_ui(self): splitter = QSplitter(Qt.Horizontal) # Left panel: Playlist self.channel_list = QListWidget() self.channel_list.itemClicked.connect(self.on_channel_clicked) splitter.addWidget(self.channel_list) # Right panel: Video on top, EPG on bottom right_panel = QWidget() right_layout = QVBoxLayout() self.video_frame = VideoFrame(self) right_layout.addWidget(self.video_frame, 3) self.error_label = QLabel("", self.video_frame) self.error_label.setAlignment(Qt.AlignCenter) self.error_label.setStyleSheet("color: red; background-color: rgba(0,0,0,150); font-size: 20px;") self.error_label.hide() self.epg_display = QLabel("EPG info will appear here") self.epg_display.setStyleSheet("background-color: #222; color: white; padding: 10px;") right_layout.addWidget(self.epg_display, 1) # Control buttons control_layout = QHBoxLayout() from PyQt5.QtWidgets import QPushButton self.btn_prev = QPushButton("⏮️") self.btn_play = QPushButton("⏯️") self.btn_next = QPushButton("⏭️") self.btn_fullscreen = QPushButton("🖵") self.btn_prev.clicked.connect(self.play_previous_channel) self.btn_play.clicked.connect(self.toggle_play_pause) self.btn_next.clicked.connect(self.play_next_channel) self.btn_fullscreen.clicked.connect(self.toggle_fullscreen) control_layout.addWidget(self.btn_prev) control_layout.addWidget(self.btn_play) control_layout.addWidget(self.btn_next) control_layout.addWidget(self.btn_fullscreen) right_layout.addLayout(control_layout) self.control_widget = QWidget() self.control_layout = QHBoxLayout(self.control_widget) self.control_layout.setContentsMargins(5, 5, 5, 5) self.btn_prev = QPushButton("⏮️") self.btn_play = QPushButton("⏯️") self.btn_next = QPushButton("⏭️") self.btn_fullscreen = QPushButton("🖵") self.btn_prev.clicked.connect(self.play_previous_channel) self.btn_play.clicked.connect(self.toggle_play_pause) self.btn_next.clicked.connect(self.play_next_channel) self.btn_fullscreen.clicked.connect(self.toggle_fullscreen) for btn in [self.btn_prev, self.btn_play, self.btn_next, self.btn_fullscreen]: self.control_layout.addWidget(btn) right_layout.addWidget(self.control_widget) self.control_widget.installEventFilter(self) self.auto_hide_timer = QTimer(self) self.auto_hide_timer.setInterval(3000) self.auto_hide_timer.timeout.connect(lambda: self.control_widget.setVisible(False)) right_panel.setLayout(right_layout) splitter.addWidget(right_panel) splitter.setSizes([400, 880]) self.setCentralWidget(splitter) self.player = VLCPlayer(self.video_frame) def on_channel_clicked(self, item): self.save_last_channel_index() from PyQt5.QtWidgets import QMessageBox QGuiApplication.setOverrideCursor(Qt.WaitCursor) url = item.data(Qt.UserRole) epg = item.data(Qt.UserRole + 1) self.epg_display.setText(f"Loading: {item.text()}\nEPG: {epg}") def try_play(attempt=1): self.player.play_stream(url) QTimer.singleShot(1000, lambda: check_status(attempt)) def check_status(attempt): width = self.player.media_player.video_get_width() if width > 0: QGuiApplication.restoreOverrideCursor() self.epg_display.setText(f"Now Playing: {item.text()}\nEPG: {epg}") self.prewarm_streams(url) # trigger background stream warming update_quality_label() elif attempt < 3: try_play(attempt + 1) else: QGuiApplication.restoreOverrideCursor() self.error_label.setText("⚠️ Stream not available: " + item.text()) self.error_label.show() QTimer.singleShot(3000, lambda: self.error_label.hide()) def update_quality_label(attempt=0): width = self.player.media_player.video_get_width() height = self.player.media_player.video_get_height() fps = self.player.media_player.get_fps() if width > 0 and height > 0: res_label = "SD" if width >= 3840: res_label = "4K" elif width >= 1920: res_label = "FHD" elif width >= 1280: res_label = "HD" quality = f"{res_label}, {fps:.2f}FPS" current_text = item.text() if '(' in current_text: base_name = current_text.split('(')[0].strip() item.setText(f"{base_name} ({quality})") elif attempt < 10: QTimer.singleShot(500, lambda: update_quality_label(attempt + 1)) try_play() def download_playlist(self): try: os.makedirs(self.php_dir, exist_ok=True) playlist_url = "https://iptv.nywebforum.com/playlist.m3u" response = requests.get(playlist_url, timeout=10) content = response.text content = content.replace( "https://iptv.nywebforum.com", f"http://localhost:8888" ) playlist_path = os.path.join(self.php_dir, "playlist.m3u") with open(playlist_path, "w", encoding="utf-8") as f: f.write(content) self.load_playlist(playlist_path) except Exception as e: print(f"Failed to download playlist: {e}") def load_playlist(self, playlist_path): try: with open(playlist_path, "r", encoding="utf-8") as f: lines = f.readlines() self.channel_list.clear() current_name = None current_logo = None current_group = "Uncategorized" self.groups = {} self.last_channel_index = None last_played_index = self.load_last_channel_index() for line in lines: line = line.strip() if line.startswith("#EXTINF:"): current_logo = "" current_group = "Uncategorized" parts = line.split(',') current_name = parts[-1].strip() if len(parts) > 1 else "Unknown" if 'tvg-logo="' in line: logo_match = re.search(r'tvg-logo="([^"]+)"', line) if logo_match: current_logo = logo_match.group(1) if 'group-title="' in line: group_match = re.search(r'group-title="([^"]+)"', line) if group_match: current_group = group_match.group(1) elif line and not line.startswith("#") and current_name: url = line item = QListWidgetItem(current_name) item.setData(Qt.UserRole, url) item.setData(Qt.UserRole + 1, "EPG info placeholder") item.setData(Qt.UserRole + 2, current_logo) item.setData(Qt.UserRole + 3, current_group) if current_logo: try: from PyQt5.QtGui import QPixmap, QIcon img_data = requests.get(current_logo, timeout=3).content pixmap = QPixmap() pixmap.loadFromData(img_data) icon = QIcon(pixmap.scaled(40, 40, Qt.KeepAspectRatio)) item.setIcon(icon) except: pass self.channel_list.addItem(item) if last_played_index is not None and self.channel_list.count() - 1 == last_played_index: self.last_channel_index = item current_name = None if self.last_channel_index: self.channel_list.setCurrentItem(self.last_channel_index) QTimer.singleShot(1000, lambda: self.on_channel_clicked(self.last_channel_index)) except Exception as e: print(f"Failed to load playlist: {e}") def start_php_server(php_dir, port=8888): import subprocess import os import sys try: os.chdir(php_dir) print(f"Starting PHP server from {php_dir} on port {port}") return subprocess.Popen( ["php", "-S", f"localhost:{port}"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0 ) except Exception as e: print(f"Failed to start PHP server: {e}") return None if __name__ == "__main__": php_dir = os.path.join(os.path.dirname(__file__), "php_files") os.makedirs(php_dir, exist_ok=True) php_process = start_php_server(php_dir) if not php_process: print("❌ Failed to start PHP server.") sys.exit(1) app = QApplication(sys.argv) window = IPTVWindow() window.show() exit_code = app.exec_() window.player.stop() window.player.release() if php_process: php_process.terminate() try: php_process.wait(timeout=2) except subprocess.TimeoutExpired: php_process.kill() sys.exit(exit_code)