import os
import sys
import time
import requests
import subprocess
import gzip
import io
from datetime import datetime, timedelta,timezone
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QVBoxLayout, QListWidget, QWidget, QHBoxLayout,
QListWidgetItem, QLabel, QSplitter, QFrame, QPushButton, QSlider, QSizePolicy,
QScrollArea,QGridLayout
)
from PyQt5.QtCore import Qt, QTimer, QEvent, QTime, QUrl, QThread, pyqtSignal
from PyQt5.QtGui import QFont,QPainter, QColor
import pytz
import vlc
import xml.etree.ElementTree as ET
class VideoFrame(QFrame):
def __init__(self, parent=None):
super().__init__(parent)
self.setStyleSheet("background-color: black;")
class VLCPlayer:
def __init__(self, video_frame):
self.instance = vlc.Instance()
self.media_player = self.instance.media_player_new()
if sys.platform == "win32":
self.media_player.set_hwnd(int(video_frame.winId()))
else:
self.media_player.set_xwindow(int(video_frame.winId()))
def play_stream(self, url):
media = self.instance.media_new(url)
media.add_option(":network-caching=1000")
self.media_player.set_media(media)
self.media_player.play()
self.media_player.audio_set_volume(70)
def stop(self):
self.media_player.stop()
def release(self):
self.media_player.release()
self.instance.release()
class EPGDownloader(QThread):
epg_downloaded = pyqtSignal(bytes)
error_occurred = pyqtSignal(str)
def __init__(self, epg_url):
super().__init__()
self.epg_url = epg_url
def run(self):
try:
response = requests.get(self.epg_url, timeout=10)
response.raise_for_status()
self.epg_downloaded.emit(response.content)
except Exception as e:
self.error_occurred.emit(f"EPG download failed: {str(e)}")
class IPTVWindow(QMainWindow):
def __init__(self, php_dir):
super().__init__()
self.setWindowTitle("IPTV Player with EPG")
self.setGeometry(100, 100, 1280, 720)
self.php_dir = php_dir
self._is_fullscreen = False
self._normal_geometry = None
self._normal_flags = None
self.current_channel_epg = None
self.epg_url = None
self.epg_data = None
self.epg_last_update = None
self.debug_log = []
self.channel_order = [] # ✅ Prevent AttributeError
self.epg_grid_window = None
self.init_ui()
self.installEventFilter(self)
self.download_playlist()
def log_debug(self, message):
"""Add debug message to log and print to console"""
timestamp = datetime.now().strftime("%H:%M:%S")
full_message = f"[{timestamp}] {message}"
self.debug_log.append(full_message)
print(full_message)
if hasattr(self, 'debug_display'):
self.debug_display.addItem(full_message)
self.debug_display.scrollToBottom()
def init_ui(self):
self.splitter = QSplitter(Qt.Horizontal)
# Left panel with channels
self.channel_list = QListWidget()
self.channel_list.itemClicked.connect(self.on_channel_clicked)
self.splitter.addWidget(self.channel_list)
# Right panel with video and EPG
right_panel = QWidget()
layout = QVBoxLayout(right_panel)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
self.video_container = QWidget()
self.video_container.setStyleSheet("background-color: black;")
self.video_container.setMinimumHeight(400)
video_container_layout = QVBoxLayout(self.video_container)
video_container_layout.setContentsMargins(0, 0, 0, 0)
self.video_frame = VideoFrame(self.video_container)
self.video_frame.mouseDoubleClickEvent = self.toggle_fullscreen
video_container_layout.addWidget(self.video_frame)
layout.addWidget(self.video_container, stretch=3)
self.player = VLCPlayer(self.video_frame)
# EPG Display with new formatting
self.epg_display = QLabel()
self.epg_display.setStyleSheet("""
background-color: #222;
color: white;
padding: 10px;
""")
self.epg_display.setWordWrap(True)
self.epg_display.setAlignment(Qt.AlignTop | Qt.AlignLeft)
layout.addWidget(self.epg_display, stretch=1)
# Timers
self.clock_timer = QTimer()
self.clock_timer.timeout.connect(self.refresh_epg_time)
self.clock_timer.start(1000)
self.epg_update_timer = QTimer()
self.epg_update_timer.timeout.connect(self.update_epg_data)
self.epg_update_timer.start(3600000) # Update EPG every hour
# Controls
self.controls = QWidget()
ctl_layout = QHBoxLayout(self.controls)
for text, slot in [("⏮️", self.play_previous_channel),
("⏯️", self.toggle_play_pause),
("⏭️", self.play_next_channel),
("🖵", self.toggle_fullscreen),
("📅", self.toggle_epg_grid),
("🔄", self.update_epg_data)]:
btn = QPushButton(text)
btn.clicked.connect(slot)
ctl_layout.addWidget(btn)
self.volume_slider = QSlider(Qt.Horizontal)
self.volume_slider.setRange(0, 200)
self.volume_slider.setValue(70)
self.volume_slider.setToolTip("Volume")
self.volume_slider.valueChanged.connect(self.set_volume)
ctl_layout.addWidget(self.volume_slider)
self.mute_button = QPushButton("🔊")
self.mute_button.setCheckable(True)
self.mute_button.clicked.connect(self.toggle_mute)
ctl_layout.addWidget(self.mute_button)
layout.addWidget(self.controls)
self.splitter.addWidget(right_panel)
self.splitter.setSizes([300, 980])
self.setCentralWidget(self.splitter)
def set_volume(self, value):
self.player.media_player.audio_set_volume(value)
if value == 0:
self.player.media_player.audio_set_mute(True)
self.mute_button.setChecked(True)
self.mute_button.setText("🔇")
else:
self.player.media_player.audio_set_mute(False)
self.mute_button.setChecked(False)
self.mute_button.setText("🔊")
def toggle_mute(self):
is_muted = self.player.media_player.audio_get_mute()
self.player.media_player.audio_set_mute(not is_muted)
self.mute_button.setText("🔇" if not is_muted else "🔊")
self.mute_button.setChecked(not is_muted)
def update_epg_info(self, name, resolution):
if not hasattr(self, 'current_channel_name'):
return
# Create formatted EPG text
epg_text = ""
# Channel name (top left) and resolution (top right)
channel_font = QFont()
channel_font.setPointSize(14)
channel_font.setBold(True)
epg_text += "
"
epg_text += f"{name} | {resolution} |
"
epg_text += "
"
# Current program
if self.current_channel_epg:
# Title (big and bold)
epg_text += f""
epg_text += f"{self.current_channel_epg.get('title', 'No Programme Information')}"
epg_text += "
"
# Time and duration
if 'start' in self.current_channel_epg and 'stop' in self.current_channel_epg:
start = self.current_channel_epg['start']
stop = self.current_channel_epg['stop']
try:
# Convert GMT times to aware datetime objects
gmt_start = datetime.strptime(start[:14], "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc)
gmt_stop = datetime.strptime(stop[:14], "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc)
# Convert to local time (Pakistan time here, change if needed)
local_tz = pytz.timezone("Asia/Karachi")
start_local = gmt_start.astimezone(local_tz)
stop_local = gmt_stop.astimezone(local_tz)
now_local = datetime.now(local_tz)
# Format time display
start_time_str = start_local.strftime("%I:%M%p").lstrip('0')
stop_time_str = stop_local.strftime("%I:%M%p").lstrip('0')
duration = int((stop_local - start_local).total_seconds() // 60)
# Progress percentage
total_seconds = (stop_local - start_local).total_seconds()
elapsed_seconds = (now_local - start_local).total_seconds()
progress_percent = max(0, min(100, int((elapsed_seconds / total_seconds) * 100))) if total_seconds > 0 else 0
# Build HTML progress bar
progress_bar_html = f"""
"""
# Add everything to epg_text
epg_text += f""
epg_text += f"{start_time_str} - {stop_time_str} ({duration} mins)"
epg_text += progress_bar_html
epg_text += "
"
except:
pass
# Description
if 'desc' in self.current_channel_epg and self.current_channel_epg['desc']:
epg_text += f""
epg_text += f"{self.current_channel_epg['desc']}"
epg_text += "
"
# Next program
if self.next_program_epg:
epg_text += f""
epg_text += f"Next: {self.next_program_epg.get('title', 'No information')}"
epg_text += "
"
if 'start' in self.next_program_epg and 'stop' in self.next_program_epg:
start = self.next_program_epg['start']
stop = self.next_program_epg['stop']
try:
# Convert GMT times to aware datetime objects
gmt_start = datetime.strptime(start[:14], "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc)
gmt_stop = datetime.strptime(stop[:14], "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc)
# Convert to local time (Pakistan time here, change if needed)
local_tz = pytz.timezone("Asia/Karachi")
start_local = gmt_start.astimezone(local_tz)
stop_local = gmt_stop.astimezone(local_tz)
# Format time display
start_time_str = start_local.strftime("%I:%M%p").lstrip('0')
stop_time_str = stop_local.strftime("%I:%M%p").lstrip('0')
epg_text += f""
epg_text += f"{start_time_str} - {stop_time_str}"
epg_text += "
"
except:
pass
else:
epg_text += ""
epg_text += "No Programme Information"
epg_text += "
"
self.epg_display.setText(epg_text)
def update_current_epg(self):
if not self.epg_data or not hasattr(self, 'current_channel_id'):
return
try:
# Get current LOCAL time
now_local = datetime.now()
# Convert to equivalent GMT time string for matching with EPG (which is in GMT)
now_gmt = now_local.astimezone(pytz.utc)
current_time = now_gmt.strftime("%Y%m%d%H%M%S")
found_current = False
found_next = False
for programme in self.epg_data.findall('.//programme'):
channel_id = programme.get('channel')
if channel_id != self.current_channel_id:
continue
start = programme.get('start')
stop = programme.get('stop')
if not found_current and start <= current_time <= stop:
# Current program
self.current_channel_epg = {
'title': programme.find('title').text if programme.find('title') is not None else 'No Programme Information',
'start': start,
'stop': stop,
'desc': programme.find('desc').text if programme.find('desc') is not None else ''
}
found_current = True
elif found_current and not found_next and start > current_time:
# Next program
self.next_program_epg = {
'title': programme.find('title').text if programme.find('title') is not None else 'No information',
'start': start,
'stop': stop
}
found_next = True
break
if not found_current:
self.current_channel_epg = None
self.next_program_epg = None
elif not found_next:
self.next_program_epg = None
# Update the display
if hasattr(self, 'current_channel_name') and hasattr(self, 'current_channel_resolution'):
self.update_epg_info(self.current_channel_name, self.current_channel_resolution)
except Exception as e:
print(f"Error updating current EPG: {str(e)}")
def refresh_epg_time(self):
if not hasattr(self, 'current_channel_name') or not hasattr(self, 'current_channel_resolution'):
return
text = self.epg_display.text()
parts = text.split("\n", 1)
if len(parts) == 2:
first_line, epg_line = parts
if "|" in first_line:
name_resolution = first_line.split("|")
if len(name_resolution) >= 2:
name = name_resolution[0].replace("", "").replace("", "").strip()
resolution = name_resolution[1].strip()
self.update_epg_info(name, resolution, self.current_channel_epg)
def update_epg_data(self):
if not self.epg_url:
self.log_debug("No EPG URL available")
return
self.log_debug(f"Downloading EPG data from: {self.epg_url}")
self.epg_downloader = EPGDownloader(self.epg_url)
self.epg_downloader.epg_downloaded.connect(self.process_epg_data)
self.epg_downloader.error_occurred.connect(self.log_debug)
self.epg_downloader.start()
def process_epg_data(self, epg_bytes):
try:
# Try to decompress the data
try:
epg_xml = gzip.decompress(epg_bytes).decode('utf-8')
self.log_debug("EPG data decompressed successfully")
except:
epg_xml = epg_bytes.decode('utf-8')
self.log_debug("EPG data was not compressed")
# Save the raw XML for debugging
debug_epg_path = os.path.join(self.php_dir, "last_epg.xml")
with open(debug_epg_path, "w", encoding="utf-8") as f:
f.write(epg_xml)
self.log_debug(f"Saved EPG data to {debug_epg_path}")
# Parse the XML
self.epg_data = ET.fromstring(epg_xml)
self.epg_last_update = datetime.now()
self.log_debug(f"EPG data parsed successfully, last update: {self.epg_last_update}")
# Update current channel's EPG if available
if hasattr(self, 'current_channel_id'):
self.update_current_epg()
except Exception as e:
self.log_debug(f"Error processing EPG data: {str(e)}")
def toggle_epg_grid(self):
if not self.epg_data:
self.log_debug("EPG data not loaded yet.")
return
if self.epg_grid_window is None:
self.epg_grid_window = EPGGridWindow(self.epg_data, self.channel_order)
if self.epg_grid_window.isVisible():
self.epg_grid_window.hide()
else:
self.epg_grid_window.show()
def detect_resolution(self, item, base):
w = self.player.media_player.video_get_width()
h = self.player.media_player.video_get_height()
fps = self.player.media_player.get_fps()
if w > 0 and h > 0:
label = "SD"
if w >= 3840:
label = "4K"
elif w >= 1920:
label = "FHD"
elif w >= 1280:
label = "HD"
info = f"{label}-{fps:.2f} FPS"
item.setText(f"{base} ({info})")
self.current_channel_name = base
self.current_channel_resolution = info
self.update_epg_info(base, info)
def on_channel_clicked(self, item: QListWidgetItem):
url = item.data(Qt.UserRole)
self.player.play_stream(url)
# Get channel ID from item data
channel_id = item.data(Qt.UserRole + 1)
if channel_id:
self.current_channel_id = channel_id
self.update_current_epg()
base = item.text().split('(')[0].strip()
item.setText(f"{base} (Loading...)")
self.current_channel_name = base
self.update_epg_info(base, "Loading...")
QTimer.singleShot(4000, lambda: self.detect_resolution(item, base))
def toggle_play_pause(self):
mp = self.player.media_player
if mp.is_playing():
mp.pause()
else:
mp.play()
def play_next_channel(self):
idx = self.channel_list.currentRow()
if idx < self.channel_list.count() - 1:
self.channel_list.setCurrentRow(idx+1)
self.on_channel_clicked(self.channel_list.currentItem())
def play_previous_channel(self):
idx = self.channel_list.currentRow()
if idx > 0:
self.channel_list.setCurrentRow(idx-1)
self.on_channel_clicked(self.channel_list.currentItem())
def toggle_fullscreen(self, event=None):
if not self._is_fullscreen:
self._normal_geometry = self.geometry()
self._normal_flags = self.windowFlags()
self.channel_list.hide()
self.epg_display.hide()
self.controls.hide()
self.setWindowFlags(self._normal_flags | Qt.FramelessWindowHint)
self.showFullScreen()
else:
self.setWindowFlags(self._normal_flags)
self.showNormal()
if self._normal_geometry:
self.setGeometry(self._normal_geometry)
self.channel_list.show()
self.epg_display.show()
self.controls.show()
self._is_fullscreen = not self._is_fullscreen
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape and self._is_fullscreen:
self.toggle_fullscreen()
else:
super().keyPressEvent(event)
def download_playlist(self):
try:
os.makedirs(self.php_dir, exist_ok=True)
url = "https://iptv.nywebforum.com/playlist.m3u"
self.log_debug(f"Downloading playlist from: {url}")
response = requests.get(url, timeout=10)
content = response.text.replace(
"https://iptv.nywebforum.com", "http://localhost:8888"
)
# Extract EPG URL from the first line if present
first_line = response.text.split('\n')[0]
if first_line.startswith("#EXTM3U") and "url-tvg=" in first_line:
self.epg_url = first_line.split('url-tvg="')[1].split('"')[0]
self.log_debug(f"Found EPG URL: {self.epg_url}")
self.update_epg_data() # Download EPG immediately
path = os.path.join(self.php_dir, "playlist.m3u")
with open(path, "w", encoding="utf-8") as f:
f.write(content)
self.log_debug(f"Playlist saved to: {path}")
self.load_playlist(path)
except Exception as e:
self.log_debug(f"Failed to download playlist: {e}")
def load_playlist(self, path):
try:
self.log_debug(f"Loading playlist from: {path}")
lines = open(path, encoding="utf-8").read().splitlines()
self.channel_list.clear()
first = None
name = None
channel_id = None
self.channel_order = [] # ✅ Reset and initialize channel order
for i, l in enumerate(lines):
if l.startswith("#EXTINF:"):
name = l.split(',')[-1].strip()
if 'tvg-id="' in l:
channel_id = l.split('tvg-id="')[1].split('"')[0]
self.log_debug(f"Found channel ID: {channel_id} for {name}")
elif l and not l.startswith("#") and name:
it = QListWidgetItem(f"{name} (Loading...)")
it.setData(Qt.UserRole, l)
if channel_id:
it.setData(Qt.UserRole + 1, channel_id)
self.channel_list.addItem(it)
self.channel_order.append((name, channel_id)) # ✅ Maintain order for EPG
if first is None:
first = it
name = None
channel_id = None
if first:
self.channel_list.setCurrentItem(first)
self.log_debug(f"Found {self.channel_list.count()} channels, selecting first one")
QTimer.singleShot(1000, lambda: self.on_channel_clicked(first))
except Exception as e:
self.log_debug(f"Failed to load playlist: {e}")
def closeEvent(self, event):
self.log_debug("Application closing - cleaning up resources")
if self.epg_grid_window and self.epg_grid_window.isVisible():
self.epg_grid_window.close()
# Stop timers
self.clock_timer.stop()
self.epg_update_timer.stop()
# Stop player
self.player.stop()
self.player.release()
# Stop any ongoing EPG download
if hasattr(self, 'epg_downloader') and self.epg_downloader.isRunning():
self.epg_downloader.quit()
self.epg_downloader.wait()
super().closeEvent(event)
class EPGGridWindow(QMainWindow):
def __init__(self, epg_data, channel_order):
super().__init__()
self.setWindowTitle("Full EPG")
self.resize(1400, 800)
self.setStyleSheet("""
background-color: #222;
color: white;
font-size: 10pt;
QLabel {
padding: 5px;
}
QScrollArea {
border: none;
}
""")
self.epg_data = epg_data
self.channel_order = channel_order
self.row_height = 60
self.timeline_slot_width = 150
# Align timeline start to previous 30-min mark
# Snap timeline_start to the closest 30-minute mark BEFORE now
now = datetime.now(pytz.timezone("Asia/Karachi"))
rounded_minute = 0 if now.minute < 30 else 30
aligned_now = now.replace(minute=rounded_minute, second=0, microsecond=0)
# Keep full EPG span: yesterday to +2 or +3 days
self.timeline_start = aligned_now - timedelta(days=1)
self.timeline_end = aligned_now + timedelta(days=2) # or timedelta(days=3)
# Build timeline list
self.timeline = []
t = self.timeline_start
while t <= self.timeline_end:
self.timeline.append(t)
t += timedelta(minutes=30)
# Main layout
self.main_widget = QWidget()
self.setCentralWidget(self.main_widget)
self.main_layout = QGridLayout(self.main_widget)
self.main_layout.setSpacing(0)
self.main_layout.setContentsMargins(0, 0, 0, 0)
# Top-left corner
self.corner_widget = QWidget()
self.corner_widget.setFixedSize(300, 40)
self.corner_widget.setStyleSheet("background-color: #333;")
self.main_layout.addWidget(self.corner_widget, 0, 0)
# Timeline header (centered across dividing lines)
self.timeline_scroll = QScrollArea()
self.timeline_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.timeline_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.timeline_scroll.setWidgetResizable(True)
self.timeline_widget = QWidget()
self.timeline_widget.setStyleSheet("background-color: #333;")
self.timeline_layout = QGridLayout(self.timeline_widget)
self.timeline_layout.setSpacing(1)
self.timeline_layout.setContentsMargins(0, 0, 0, 0)
for i in range(len(self.timeline) - 1):
t = self.timeline[i + 1] # Label sits above dividing line
label = QLabel(f"{t.strftime('%I:%M %p').lstrip('0')}")
label.setAlignment(Qt.AlignHCenter | Qt.AlignBottom)
label.setFixedHeight(40)
label.setStyleSheet("background-color: #333; font-weight: bold;")
self.timeline_layout.addWidget(label, 0, i, 1, 2)
# Ensure columns align with program grid
for col in range(len(self.timeline)):
self.timeline_layout.setColumnMinimumWidth(col, self.timeline_slot_width)
self.timeline_scroll.setWidget(self.timeline_widget)
self.main_layout.addWidget(self.timeline_scroll, 0, 1)
# Channel names (left)
self.channel_scroll = QScrollArea()
self.channel_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.channel_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.channel_scroll.setWidgetResizable(True)
self.channel_widget = QWidget()
self.channel_layout = QVBoxLayout(self.channel_widget)
self.channel_layout.setSpacing(1)
self.channel_layout.setContentsMargins(0, 0, 0, 0)
for row, (ch_name, ch_id) in enumerate(self.channel_order):
label = QLabel(ch_name)
label.setWordWrap(True)
label.setStyleSheet(f"background-color: {'#333' if row % 2 else '#444'};")
label.setFixedHeight(self.row_height)
self.channel_layout.addWidget(label)
self.channel_scroll.setWidget(self.channel_widget)
self.main_layout.addWidget(self.channel_scroll, 1, 0)
# Program grid (right bottom)
self.program_scroll = QScrollArea()
self.program_scroll.setWidgetResizable(True)
self.program_widget = QWidget()
self.epg_layout = QGridLayout(self.program_widget)
self.epg_layout.setSpacing(1)
self.epg_layout.setContentsMargins(0, 0, 0, 0)
# ✅ Apply fixed column widths after epg_layout is ready
for col in range(len(self.timeline)):
self.epg_layout.setColumnMinimumWidth(col, self.timeline_slot_width)
for row, (ch_name, ch_id) in enumerate(self.channel_order):
self.add_channel_epg(row, ch_id)
self.program_scroll.setWidget(self.program_widget)
self.main_layout.addWidget(self.program_scroll, 1, 1)
# Synchronize scrolling
self.program_scroll.horizontalScrollBar().valueChanged.connect(
self.timeline_scroll.horizontalScrollBar().setValue
)
self.timeline_scroll.horizontalScrollBar().valueChanged.connect(
self.program_scroll.horizontalScrollBar().setValue
)
self.program_scroll.verticalScrollBar().valueChanged.connect(
self.channel_scroll.verticalScrollBar().setValue
)
self.channel_scroll.verticalScrollBar().valueChanged.connect(
self.program_scroll.verticalScrollBar().setValue
)
# Close button
self.close_btn = QPushButton("❌ Close EPG")
self.close_btn.setStyleSheet("""
font-size: 14px;
background-color: red;
color: white;
padding: 5px;
margin: 5px;
""")
self.close_btn.clicked.connect(self.close)
self.main_layout.addWidget(self.close_btn, 2, 0, 1, 2)
# Stretch factors
self.main_layout.setColumnStretch(0, 0)
self.main_layout.setColumnStretch(1, 1)
self.main_layout.setRowStretch(0, 0)
self.main_layout.setRowStretch(1, 1)
# Clock in corner
self.clock_label = QLabel()
self.clock_label.setStyleSheet("background-color: #333; font-weight: bold;")
self.clock_label.setAlignment(Qt.AlignCenter)
self.corner_widget.layout = QVBoxLayout(self.corner_widget)
self.corner_widget.layout.addWidget(self.clock_label)
self.update_clock()
self.clock_timer = QTimer(self)
self.clock_timer.timeout.connect(self.update_clock)
self.clock_timer.start(1000)
# Auto-scroll to current time - 30 min
target_time = datetime.now(pytz.timezone("Asia/Karachi")) - timedelta(minutes=30)
start_index = max(0, int((target_time - self.timeline_start).total_seconds() // 1800))
x_pos = start_index * self.timeline_slot_width
QTimer.singleShot(100, lambda: self.program_scroll.horizontalScrollBar().setValue(x_pos))
# Overlay for current time line
self.overlay = QWidget(self.program_widget)
self.overlay.setAttribute(Qt.WA_TransparentForMouseEvents)
self.overlay.setStyleSheet("background: transparent;")
self.overlay.raise_()
self.overlay.resize(self.program_widget.size())
self.overlay.paintEvent = self.paint_now_line
self.program_widget.resizeEvent = lambda e: self.overlay.resize(self.program_widget.size())
# Timer to update vertical line
self.now_line_timer = QTimer(self)
self.now_line_timer.timeout.connect(self.overlay.update)
self.now_line_timer.start(60000) # Every minute
QTimer.singleShot(200, self.overlay.update)
def update_clock(self):
now = datetime.now(pytz.timezone("Asia/Karachi"))
self.clock_label.setText(now.strftime("%a %b %d, %I:%M:%S %p"))
def add_channel_epg(self, row, channel_id):
bg = "#333" if row % 2 else "#444"
local_tz = pytz.timezone("Asia/Karachi")
now = datetime.now(local_tz)
programs = [p for p in self.epg_data.findall(".//programme") if p.get("channel") == channel_id]
# If no EPG at all, fill entire row with repeating "No Program Info"
if not programs:
current_col = 0
while current_col < len(self.timeline):
block = min(len(self.timeline) - current_col, 4)
filler = TimelineLabel(
text="No Program Information Available",
start_offset=current_col,
duration=block,
parent_grid=self,
timeline_slot_width=self.timeline_slot_width
)
filler.setFixedHeight(self.row_height)
self.epg_layout.addWidget(filler, row, current_col, 1, block)
current_col += block
return
# Convert all valid program blocks
cells = []
for prog in programs:
try:
start = datetime.strptime(prog.get("start")[:14], "%Y%m%d%H%M%S").replace(tzinfo=pytz.utc).astimezone(local_tz)
stop = datetime.strptime(prog.get("stop")[:14], "%Y%m%d%H%M%S").replace(tzinfo=pytz.utc).astimezone(local_tz)
if stop <= self.timeline_start or start >= self.timeline_end:
continue
title = prog.findtext("title", "No Title")
# Clamp within timeline
start_offset = max(0, int((start - self.timeline_start).total_seconds() // 1800))
end_offset = min(len(self.timeline), int((stop - self.timeline_start).total_seconds() // 1800))
duration = max(1, end_offset - start_offset)
cells.append((start_offset, duration, title))
except:
continue
cells.sort()
current_col = 0
for start_offset, duration, title in cells:
# Fill gap before program with "No Info"
if start_offset > current_col:
gap_duration = start_offset - current_col
while gap_duration > 0:
block = min(gap_duration, 4)
filler = TimelineLabel(
text="No Program Information Available",
start_offset=current_col,
duration=block,
parent_grid=self,
timeline_slot_width=self.timeline_slot_width
)
filler.setFixedHeight(self.row_height)
self.epg_layout.addWidget(filler, row, current_col, 1, block)
current_col += block
gap_duration -= block
# Actual program cell
cell = TimelineLabel(
text=title,
start_offset=start_offset,
duration=duration,
parent_grid=self,
timeline_slot_width=self.timeline_slot_width
)
cell.setFixedHeight(self.row_height)
self.epg_layout.addWidget(cell, row, start_offset, 1, duration)
current_col = start_offset + duration
# Fill remaining timeline with "No Info"
while current_col < len(self.timeline):
block = min(len(self.timeline) - current_col, 4)
filler = TimelineLabel(
text="No Program Information Available",
start_offset=current_col,
duration=block,
parent_grid=self,
timeline_slot_width=self.timeline_slot_width
)
filler.setFixedHeight(self.row_height)
self.epg_layout.addWidget(filler, row, current_col, 1, block)
current_col += block
def build_timeline_header(self):
# Timeline scroll area
self.timeline_scroll = QScrollArea()
self.timeline_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.timeline_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.timeline_scroll.setWidgetResizable(True)
# Timeline widget and layout
self.timeline_widget = QWidget()
self.timeline_widget.setStyleSheet("background-color: #333;")
self.timeline_layout = QGridLayout(self.timeline_widget)
self.timeline_layout.setSpacing(1)
self.timeline_layout.setContentsMargins(0, 0, 0, 0)
# Create labels centered across dividing lines
for i in range(len(self.timeline) - 1):
t = self.timeline[i + 1] # Label marks the dividing line
label = QLabel(f"{t.strftime('%I:%M %p').lstrip('0')}")
label.setAlignment(Qt.AlignHCenter | Qt.AlignBottom)
label.setFixedHeight(40)
label.setStyleSheet("background-color: #333; font-weight: bold;")
# Add label with column span = 2
self.timeline_layout.addWidget(label, 0, i, 1, 2)
# Ensure each column in timeline matches program grid column width
for col in range(len(self.timeline)):
self.timeline_layout.setColumnMinimumWidth(col, self.timeline_slot_width)
self.timeline_scroll.setWidget(self.timeline_widget)
self.main_layout.addWidget(self.timeline_scroll, 0, 1)
def paint_now_line(self, event):
from PyQt5.QtGui import QPainter, QPen
painter = QPainter(self.overlay)
pen = QPen(Qt.red, 2)
painter.setPen(pen)
now = datetime.now(pytz.timezone("Asia/Karachi"))
seconds_since_start = (now - self.timeline_start).total_seconds()
x = (seconds_since_start / 1800) * self.timeline_slot_width
painter.drawLine(int(x), 0, int(x), self.overlay.height())
def update_now_line_position(self):
now = datetime.now(pytz.timezone("Asia/Karachi"))
offset = int((now - self.timeline_start).total_seconds() // 1800)
x = offset * self.timeline_slot_width
self.now_line.move(x, 0)
self.now_line.setFixedHeight(self.program_widget.height())
class TimelineLabel(QLabel):
def __init__(self, text, start_offset, duration, parent_grid, timeline_slot_width):
super().__init__()
self.full_text = text
self.start_offset = start_offset
self.duration = duration
self.parent_grid = parent_grid
self.timeline_slot_width = timeline_slot_width
self.setStyleSheet("""
border: 1px solid #555;
color: white;
""")
self.setWordWrap(False)
self.setAlignment(Qt.AlignVCenter)
self.setText("") # We draw text ourselves
def paintEvent(self, event):
super().paintEvent(event)
painter = QPainter(self)
# Get visible scroll offset in pixels
scroll_px = self.parent_grid.program_scroll.horizontalScrollBar().value()
cell_x = self.start_offset * self.timeline_slot_width
visible_x = cell_x - scroll_px
# Draw text if any part of the cell is visible
if visible_x + self.width() > 0 and visible_x < self.parent_grid.program_scroll.viewport().width():
painter.setPen(QColor("white"))
# Draw text inside visible area
painter.drawText(self.rect(), Qt.AlignCenter, self.full_text)
def start_php_server(php_dir, port=8888):
try:
os.chdir(php_dir)
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_proc = start_php_server(php_dir)
app = QApplication(sys.argv)
window = IPTVWindow(php_dir)
window.show()
exit_code = app.exec_()
# Ensure clean exit
if php_proc:
php_proc.terminate()
php_proc.wait(2)
sys.exit(exit_code)