|
|
|
@ -11,6 +11,7 @@ import urwid |
|
|
|
from urllib.error import URLError |
|
|
|
import threading |
|
|
|
import time |
|
|
|
import urllib.parse |
|
|
|
|
|
|
|
# Load configuration from JSON file |
|
|
|
try: |
|
|
|
@ -21,6 +22,7 @@ try: |
|
|
|
USER_AGENT = config.get('user_agent', "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") |
|
|
|
UPDATE_INTERVAL = config.get('update_interval', 900) |
|
|
|
MONITOR = config.get('monitor', None) |
|
|
|
DEBUG_MODE = config.get('debug_mode', False) |
|
|
|
|
|
|
|
# Base MPV command with monitor options if specified |
|
|
|
MPV_BASE = ["mpv"] |
|
|
|
@ -38,6 +40,7 @@ except FileNotFoundError: |
|
|
|
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" |
|
|
|
UPDATE_INTERVAL = 900 |
|
|
|
MONITOR = None |
|
|
|
DEBUG_MODE = False |
|
|
|
MPV_BASE = [ |
|
|
|
"mpv", |
|
|
|
"--really-quiet", |
|
|
|
@ -45,8 +48,9 @@ except FileNotFoundError: |
|
|
|
"--force-window=immediate" |
|
|
|
] |
|
|
|
|
|
|
|
# Color Palette remains the same |
|
|
|
# Enhanced color palette with animation colors |
|
|
|
PALETTE = [ |
|
|
|
# Original colors |
|
|
|
('header', 'white', 'dark blue'), |
|
|
|
('footer', 'white', 'dark blue'), |
|
|
|
('channel', 'black', 'light cyan'), |
|
|
|
@ -59,9 +63,29 @@ PALETTE = [ |
|
|
|
('time', 'yellow', ''), |
|
|
|
('title', 'bold', ''), |
|
|
|
('update', 'light blue', ''), |
|
|
|
|
|
|
|
# Animation colors |
|
|
|
('splash_1', 'light red', ''), |
|
|
|
('splash_2', 'yellow', ''), |
|
|
|
('splash_3', 'light green', ''), |
|
|
|
('splash_4', 'light cyan', ''), |
|
|
|
('splash_5', 'light blue', ''), |
|
|
|
('splash_6', 'light magenta', ''), |
|
|
|
('highlight', 'white,bold', 'dark blue'), |
|
|
|
('loading', 'yellow', ''), |
|
|
|
('pulse_1', 'dark cyan', ''), |
|
|
|
('pulse_2', 'light cyan', ''), |
|
|
|
('pulse_3', 'white', ''), |
|
|
|
('flash', 'white,bold', 'dark green'), |
|
|
|
('fade', 'dark gray', ''), |
|
|
|
] |
|
|
|
|
|
|
|
# ASCII Art |
|
|
|
# Function for debug prints that only outputs when debug mode is enabled |
|
|
|
def debug_print(*args, **kwargs): |
|
|
|
if DEBUG_MODE: |
|
|
|
print(*args, **kwargs) |
|
|
|
|
|
|
|
# ASCII Art and Animation Frames |
|
|
|
BANNER = r""" |
|
|
|
__________._____. ___. .__ __ _______________ ____ |
|
|
|
\______ \__\_ |__\_ |__ |__|/ |_ \__ ___/\ \ / / |
|
|
|
@ -71,6 +95,79 @@ __________._____. ___. .__ __ _______________ ____ |
|
|
|
\/ \/ \/ |
|
|
|
""" |
|
|
|
|
|
|
|
# Animation frames for the splash screen |
|
|
|
SPLASH_FRAMES = [ |
|
|
|
# Frame 1 - Just B |
|
|
|
r""" |
|
|
|
_________ |
|
|
|
|\\ | |
|
|
|
| \\ | |
|
|
|
| \\_____| |
|
|
|
| |_____| |
|
|
|
| | | |
|
|
|
|___|_____| |
|
|
|
|
|
|
|
""", |
|
|
|
# Frame 2 - BI |
|
|
|
r""" |
|
|
|
_________ ___ |
|
|
|
|\\ | | | |
|
|
|
| \\ | | | |
|
|
|
| \\_____| | | |
|
|
|
| |_____| | | |
|
|
|
| | | | |
|
|
|
|___|_____| |___| |
|
|
|
|
|
|
|
""", |
|
|
|
# Frame 3 - BIB |
|
|
|
r""" |
|
|
|
_________ ___ _________ |
|
|
|
|\\ | | | |\\ | |
|
|
|
| \\ | | | | \\ | |
|
|
|
| \\_____| | | | \\_____| |
|
|
|
| |_____| | | | |_____| |
|
|
|
| | | | | | | |
|
|
|
|___|_____| |___| |___|_____| |
|
|
|
|
|
|
|
""", |
|
|
|
# Frame 4 - BIBB |
|
|
|
r""" |
|
|
|
_________ ___ _________ _________ |
|
|
|
|\\ | | | |\\ | |\\ | |
|
|
|
| \\ | | | | \\ | | \\ | |
|
|
|
| \\_____| | | | \\_____| | \\_____| |
|
|
|
| |_____| | | | |_____| | |_____| |
|
|
|
| | | | | | | | | | |
|
|
|
|___|_____| |___| |___|_____| |___|_____| |
|
|
|
|
|
|
|
""", |
|
|
|
# Frame 5 - BIBBI |
|
|
|
r""" |
|
|
|
_________ ___ _________ _________ ___ |
|
|
|
|\\ | | | |\\ | |\\ | | | |
|
|
|
| \\ | | | | \\ | | \\ | | | |
|
|
|
| \\_____| | | | \\_____| | \\_____| | | |
|
|
|
| |_____| | | | |_____| | |_____| | | |
|
|
|
| | | | | | | | | | | |
|
|
|
|___|_____| |___| |___|_____| |___|_____| |___| |
|
|
|
|
|
|
|
""", |
|
|
|
# Frame 6 - BIBBIT |
|
|
|
r""" |
|
|
|
_________ ___ _________ _________ ___ _______ |
|
|
|
|\\ | | | |\\ | |\\ | | | | | |
|
|
|
| \\ | | | | \\ | | \\ | | | |_ _| |
|
|
|
| \\_____| | | | \\_____| | \\_____| | | | | |
|
|
|
| |_____| | | | |_____| | |_____| | | | | |
|
|
|
| | | | | | | | | | | | | |
|
|
|
|___|_____| |___| |___|_____| |___|_____| |___| |_______| |
|
|
|
|
|
|
|
""", |
|
|
|
] |
|
|
|
|
|
|
|
# Loading animation characters |
|
|
|
LOADING_CHARS = ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'] |
|
|
|
|
|
|
|
class ChannelButton(urwid.Button): |
|
|
|
def __init__(self, channel, *args, **kwargs): |
|
|
|
self.channel = channel |
|
|
|
@ -121,6 +218,9 @@ class SettingsMenu: |
|
|
|
rb_2 = urwid.RadioButton(self.monitor_group, "Monitor 2", state=(monitor_value == 2)) |
|
|
|
rb_3 = urwid.RadioButton(self.monitor_group, "Monitor 3", state=(monitor_value == 3)) |
|
|
|
|
|
|
|
# Debug mode checkbox |
|
|
|
self.debug_checkbox = urwid.CheckBox("Enable Debug Mode", state=self.config.get('debug_mode', False)) |
|
|
|
|
|
|
|
# Create styled form sections |
|
|
|
form_items = [ |
|
|
|
# Header section |
|
|
|
@ -154,6 +254,12 @@ class SettingsMenu: |
|
|
|
urwid.AttrMap(rb_3, None, 'channel_focus'), |
|
|
|
urwid.Divider(), |
|
|
|
|
|
|
|
# Debug settings section |
|
|
|
urwid.AttrMap(urwid.Text("DEBUG SETTINGS", align='left'), 'program_now'), |
|
|
|
urwid.Divider(), |
|
|
|
urwid.AttrMap(self.debug_checkbox, None, 'channel_focus'), |
|
|
|
urwid.Divider(), |
|
|
|
|
|
|
|
# Action buttons |
|
|
|
urwid.AttrMap(urwid.Divider("─"), 'divider'), |
|
|
|
urwid.Divider(), |
|
|
|
@ -183,7 +289,8 @@ class SettingsMenu: |
|
|
|
'm3u_url': self.m3u_edit.edit_text.strip(), |
|
|
|
'xmltv_url': self.xmltv_edit.edit_text.strip(), |
|
|
|
'user_agent': self.user_agent_edit.edit_text.strip(), |
|
|
|
'update_interval': self.update_interval_edit.value() |
|
|
|
'update_interval': self.update_interval_edit.value(), |
|
|
|
'debug_mode': self.debug_checkbox.state |
|
|
|
} |
|
|
|
|
|
|
|
# Handle monitor setting from radio buttons |
|
|
|
@ -256,18 +363,25 @@ class IPTVPlayer: |
|
|
|
self.update_running = True |
|
|
|
self.settings_menu = None |
|
|
|
self.main_widget = None |
|
|
|
self.animate_startup = False # Disable animations for better performance |
|
|
|
self.splash_screen = None |
|
|
|
self.load_data() |
|
|
|
self.setup_ui() |
|
|
|
|
|
|
|
def display_splash_screen(self): |
|
|
|
"""Disabled splash screen for better performance""" |
|
|
|
# Skip splash screen completely for better performance |
|
|
|
pass |
|
|
|
|
|
|
|
def load_data(self): |
|
|
|
print("\n[DEBUG] Loading IPTV data...") |
|
|
|
debug_print("\n[DEBUG] Loading IPTV data...") |
|
|
|
|
|
|
|
# Load M3U playlist |
|
|
|
try: |
|
|
|
req = Request(M3U_URL, headers={'User-Agent': USER_AGENT}) |
|
|
|
with urlopen(req, timeout=10) as response: |
|
|
|
m3u_data = response.read().decode('utf-8') |
|
|
|
print(f"[DEBUG] Received M3U data (length: {len(m3u_data)} bytes)") |
|
|
|
debug_print(f"[DEBUG] Received M3U data (length: {len(m3u_data)} bytes)") |
|
|
|
|
|
|
|
# Parse M3U |
|
|
|
lines = m3u_data.split('\n') |
|
|
|
@ -293,7 +407,10 @@ class IPTVPlayer: |
|
|
|
'current_show': None, |
|
|
|
'next_show': None |
|
|
|
} |
|
|
|
elif line.startswith('http://'): |
|
|
|
elif (line.startswith('http://') or line.startswith('https://') or |
|
|
|
line.startswith('rtmp://') or line.startswith('udp://') or |
|
|
|
line.startswith('rtp://') or line.startswith('mms://') or |
|
|
|
line.startswith('rtsp://') or line.startswith('file://')): |
|
|
|
if current_channel: |
|
|
|
current_channel['url'] = line |
|
|
|
self.channels.append(current_channel) |
|
|
|
@ -303,7 +420,7 @@ class IPTVPlayer: |
|
|
|
print("[WARNING] No channels found in M3U file!") |
|
|
|
self.add_error_channel("No channels found in playlist") |
|
|
|
else: |
|
|
|
print(f"[DEBUG] Successfully loaded {len(self.channels)} channels") |
|
|
|
debug_print(f"[DEBUG] Successfully loaded {len(self.channels)} channels") |
|
|
|
|
|
|
|
except Exception as e: |
|
|
|
print(f"[ERROR] Failed to load M3U: {str(e)}") |
|
|
|
@ -315,7 +432,7 @@ class IPTVPlayer: |
|
|
|
req = Request(XMLTV_URL, headers={'User-Agent': USER_AGENT}) |
|
|
|
with urlopen(req, timeout=10) as response: |
|
|
|
xml_data = response.read().decode('utf-8') |
|
|
|
print(f"[DEBUG] Received XMLTV data (length: {len(xml_data)} bytes)") |
|
|
|
debug_print(f"[DEBUG] Received XMLTV data (length: {len(xml_data)} bytes)") |
|
|
|
|
|
|
|
root = ET.fromstring(xml_data) |
|
|
|
for programme in root.findall('programme'): |
|
|
|
@ -325,20 +442,170 @@ class IPTVPlayer: |
|
|
|
title_elem = programme.find('title') |
|
|
|
desc_elem = programme.find('desc') |
|
|
|
|
|
|
|
# Extract additional episode information |
|
|
|
episode_num_elem = programme.find('episode-num') |
|
|
|
episode_num = "" |
|
|
|
season = None |
|
|
|
episode = None |
|
|
|
|
|
|
|
# Parse episode numbering systems |
|
|
|
if episode_num_elem is not None and episode_num_elem.text: |
|
|
|
episode_num = episode_num_elem.text |
|
|
|
debug_print(f"[DEBUG] Found episode number: {episode_num}, system: {episode_num_elem.get('system')}") |
|
|
|
|
|
|
|
# Extract season and episode from xmltv_ns format (common format: S.E.P/TOTAL where S=season, E=episode, P=part) |
|
|
|
if episode_num_elem.get('system') == 'xmltv_ns': |
|
|
|
# Format example: "2.9." means Season 3, Episode 10 (zero-based) |
|
|
|
parts = episode_num.split('.') |
|
|
|
debug_print(f"[DEBUG] Split episode_num into parts: {parts}") |
|
|
|
if len(parts) >= 2: |
|
|
|
try: |
|
|
|
# XMLTV uses zero-based numbering, so add 1 |
|
|
|
if parts[0].strip(): |
|
|
|
season = int(parts[0]) + 1 |
|
|
|
debug_print(f"[DEBUG] Parsed season: {season}") |
|
|
|
if parts[1].strip(): |
|
|
|
episode = int(parts[1]) + 1 |
|
|
|
debug_print(f"[DEBUG] Parsed episode: {episode}") |
|
|
|
except ValueError as e: |
|
|
|
debug_print(f"[DEBUG] Error parsing season/episode: {e}") |
|
|
|
# Also try to find other common formats |
|
|
|
elif episode_num.lower().startswith('s') and 'e' in episode_num.lower(): |
|
|
|
# Format like "S01E05" |
|
|
|
try: |
|
|
|
s_part = episode_num.lower().split('e')[0].strip('s') |
|
|
|
e_part = episode_num.lower().split('e')[1].strip() |
|
|
|
if s_part.isdigit(): |
|
|
|
season = int(s_part) |
|
|
|
debug_print(f"[DEBUG] Parsed season from SxxExx format: {season}") |
|
|
|
if e_part.isdigit(): |
|
|
|
episode = int(e_part) |
|
|
|
debug_print(f"[DEBUG] Parsed episode from SxxExx format: {episode}") |
|
|
|
except Exception as e: |
|
|
|
debug_print(f"[DEBUG] Error parsing SxxExx format: {e}") |
|
|
|
# Try digit format (like "105" for S01E05) |
|
|
|
elif episode_num.isdigit() and len(episode_num) >= 3: |
|
|
|
try: |
|
|
|
if len(episode_num) == 3: |
|
|
|
season = int(episode_num[0]) |
|
|
|
episode = int(episode_num[1:]) |
|
|
|
elif len(episode_num) == 4: |
|
|
|
season = int(episode_num[:2]) |
|
|
|
episode = int(episode_num[2:]) |
|
|
|
debug_print(f"[DEBUG] Parsed from numeric format: S{season}E{episode}") |
|
|
|
except Exception as e: |
|
|
|
debug_print(f"[DEBUG] Error parsing numeric format: {e}") |
|
|
|
|
|
|
|
# For debugging: also check direct episode and season tags |
|
|
|
season_elem = programme.find('season-num') |
|
|
|
if season_elem is not None and season_elem.text: |
|
|
|
try: |
|
|
|
season = int(season_elem.text) |
|
|
|
debug_print(f"[DEBUG] Found direct season tag: {season}") |
|
|
|
except ValueError: |
|
|
|
pass |
|
|
|
|
|
|
|
episode_elem = programme.find('episode-num') |
|
|
|
if episode_elem is not None and episode_elem.text and episode_elem.get('system') == 'onscreen': |
|
|
|
debug_print(f"[DEBUG] Found onscreen episode format: {episode_elem.text}") |
|
|
|
# Try to extract numbers from onscreen format (like "Episode 5") |
|
|
|
try: |
|
|
|
num_match = re.search(r'(\d+)', episode_elem.text) |
|
|
|
if num_match and episode is None: |
|
|
|
episode = int(num_match.group(1)) |
|
|
|
debug_print(f"[DEBUG] Extracted episode number from onscreen format: {episode}") |
|
|
|
except Exception as e: |
|
|
|
debug_print(f"[DEBUG] Error parsing onscreen format: {e}") |
|
|
|
|
|
|
|
# Also check if there's a direct 'episode' tag |
|
|
|
direct_episode_elem = programme.find('episode') |
|
|
|
if direct_episode_elem is not None and direct_episode_elem.text: |
|
|
|
try: |
|
|
|
episode = int(direct_episode_elem.text) |
|
|
|
debug_print(f"[DEBUG] Found direct episode tag: {episode}") |
|
|
|
except ValueError: |
|
|
|
pass |
|
|
|
|
|
|
|
# Detect if we have the season/episode directly in the title |
|
|
|
title_text = title_elem.text if title_elem is not None else "" |
|
|
|
if title_text and (season is None or episode is None): |
|
|
|
# Look for patterns like "S01E05" in the title |
|
|
|
se_match = re.search(r'S(\d+)E(\d+)', title_text, re.IGNORECASE) |
|
|
|
if se_match: |
|
|
|
season = int(se_match.group(1)) |
|
|
|
episode = int(se_match.group(2)) |
|
|
|
debug_print(f"[DEBUG] Extracted from title: S{season}E{episode} from '{title_text}'") |
|
|
|
# Check format like "1x05" |
|
|
|
elif 'x' in title_text.lower(): |
|
|
|
x_match = re.search(r'(\d+)x(\d+)', title_text, re.IGNORECASE) |
|
|
|
if x_match: |
|
|
|
season = int(x_match.group(1)) |
|
|
|
episode = int(x_match.group(2)) |
|
|
|
debug_print(f"[DEBUG] Extracted from title 'x' format: S{season}E{episode} from '{title_text}'") |
|
|
|
# Check for "Season X Episode Y" text |
|
|
|
elif 'season' in title_text.lower() and 'episode' in title_text.lower(): |
|
|
|
debug_print(f"[DEBUG] Title contains 'season' and 'episode': '{title_text}'") |
|
|
|
# Try to extract season and episode numbers |
|
|
|
season_match = re.search(r'season\s+(\d+)', title_text, re.IGNORECASE) |
|
|
|
episode_match = re.search(r'episode\s+(\d+)', title_text, re.IGNORECASE) |
|
|
|
|
|
|
|
if season_match: |
|
|
|
season = int(season_match.group(1)) |
|
|
|
debug_print(f"[DEBUG] Extracted season from text: {season}") |
|
|
|
if episode_match: |
|
|
|
episode = int(episode_match.group(1)) |
|
|
|
debug_print(f"[DEBUG] Extracted episode from text: {episode}") |
|
|
|
# Check for Episode number in brackets like "Show Name (5)" |
|
|
|
elif re.search(r'\(\s*\d+\s*\)', title_text): |
|
|
|
num_match = re.search(r'\(\s*(\d+)\s*\)', title_text) |
|
|
|
if num_match: |
|
|
|
# Assume this is an episode number if we don't have one yet |
|
|
|
if episode is None: |
|
|
|
episode = int(num_match.group(1)) |
|
|
|
debug_print(f"[DEBUG] Extracted episode from brackets: {episode}") |
|
|
|
# Check for common patterns like "E05" without season |
|
|
|
elif re.search(r'E\d+', title_text, re.IGNORECASE) and episode is None: |
|
|
|
e_match = re.search(r'E(\d+)', title_text, re.IGNORECASE) |
|
|
|
if e_match: |
|
|
|
episode = int(e_match.group(1)) |
|
|
|
debug_print(f"[DEBUG] Extracted episode from E-format: {episode}") |
|
|
|
|
|
|
|
if season is not None or episode is not None: |
|
|
|
debug_print(f"[DEBUG] Final season/episode for '{title_text}': S{season}E{episode}") |
|
|
|
|
|
|
|
# Get original air date if available |
|
|
|
date_elem = programme.find('date') |
|
|
|
air_date = date_elem.text if date_elem is not None else None |
|
|
|
|
|
|
|
if channel_id not in self.programs: |
|
|
|
self.programs[channel_id] = [] |
|
|
|
|
|
|
|
# Create formatted show title with season/episode if available |
|
|
|
formatted_title = title_elem.text if title_elem is not None else "No Title" |
|
|
|
|
|
|
|
self.programs[channel_id].append({ |
|
|
|
'start': start, |
|
|
|
'stop': stop, |
|
|
|
'title': title_elem.text if title_elem is not None else "No Title", |
|
|
|
'desc': desc_elem.text if desc_elem is not None else "" |
|
|
|
'title': formatted_title, |
|
|
|
'desc': desc_elem.text if desc_elem is not None else "", |
|
|
|
'season': season, |
|
|
|
'episode': episode, |
|
|
|
'episode_num': episode_num, |
|
|
|
'air_date': air_date |
|
|
|
}) |
|
|
|
print(f"[DEBUG] Loaded EPG data for {len(self.programs)} channels") |
|
|
|
debug_print(f"[DEBUG] Loaded EPG data for {len(self.programs)} channels") |
|
|
|
|
|
|
|
# Pre-load current and next shows for each channel |
|
|
|
self.update_current_shows() |
|
|
|
|
|
|
|
# Show a sample of program data with season/episode info for debugging |
|
|
|
for channel_id, programs in self.programs.items(): |
|
|
|
if programs and len(programs) > 0: |
|
|
|
sample = programs[0] |
|
|
|
if sample.get('season') is not None or sample.get('episode') is not None: |
|
|
|
debug_print(f"[DEBUG] Sample program: Title={sample.get('title')}, Season={sample.get('season')}, Episode={sample.get('episode')}, Episode_num={sample.get('episode_num')}") |
|
|
|
break |
|
|
|
|
|
|
|
except Exception as e: |
|
|
|
print(f"[WARNING] Failed to load EPG: {str(e)}") |
|
|
|
|
|
|
|
@ -363,6 +630,8 @@ class IPTVPlayer: |
|
|
|
break |
|
|
|
|
|
|
|
def add_error_channel(self, message): |
|
|
|
# Clear existing channels before adding error channel |
|
|
|
self.channels = [] |
|
|
|
self.channels.append({ |
|
|
|
'id': "error_channel", |
|
|
|
'name': f"Error: {message}", |
|
|
|
@ -379,6 +648,33 @@ class IPTVPlayer: |
|
|
|
return datetime.strptime(time_str[:14], "%Y%m%d%H%M%S") |
|
|
|
except: |
|
|
|
return datetime.now() |
|
|
|
|
|
|
|
def generate_imdb_url(self, show): |
|
|
|
"""Generate IMDb search URL for a show or specific episode""" |
|
|
|
# Base title for search |
|
|
|
title = show.get('title', '') |
|
|
|
if not title: |
|
|
|
return None |
|
|
|
|
|
|
|
# Check if we have season and episode information |
|
|
|
season = show.get('season') |
|
|
|
episode = show.get('episode') |
|
|
|
|
|
|
|
# Advanced search for TV episodes is more accurate for specific episodes |
|
|
|
if season is not None and episode is not None: |
|
|
|
# For episodes, use IMDb's advanced title search which gives better results |
|
|
|
# Format properly for the advanced search with title, season and episode |
|
|
|
show_title = urllib.parse.quote_plus(title) |
|
|
|
|
|
|
|
# Use IMDb's advanced search for TV episodes |
|
|
|
# This directly targets the episode search with better filtering |
|
|
|
return f"https://www.imdb.com/search/title/?title={show_title}&title_type=tv_episode&season={season}&episode={episode}" |
|
|
|
else: |
|
|
|
# For shows without episode info, use regular search |
|
|
|
encoded_query = urllib.parse.quote_plus(title) |
|
|
|
return f"https://www.imdb.com/find?q={encoded_query}&s=tt&ttype=tv" |
|
|
|
|
|
|
|
# Removed clickable link function since it doesn't work in Windows CMD |
|
|
|
|
|
|
|
def setup_ui(self): |
|
|
|
# Create channel list with current show info |
|
|
|
@ -501,12 +797,13 @@ class IPTVPlayer: |
|
|
|
json.dump(new_config, config_file, indent=4) |
|
|
|
|
|
|
|
# Update global variables |
|
|
|
global M3U_URL, XMLTV_URL, USER_AGENT, UPDATE_INTERVAL, MONITOR, MPV_BASE |
|
|
|
global M3U_URL, XMLTV_URL, USER_AGENT, UPDATE_INTERVAL, MONITOR, MPV_BASE, DEBUG_MODE |
|
|
|
M3U_URL = new_config['m3u_url'] |
|
|
|
XMLTV_URL = new_config['xmltv_url'] |
|
|
|
USER_AGENT = new_config['user_agent'] |
|
|
|
UPDATE_INTERVAL = new_config['update_interval'] |
|
|
|
MONITOR = new_config['monitor'] |
|
|
|
DEBUG_MODE = new_config['debug_mode'] |
|
|
|
|
|
|
|
# Update MPV base command directly from new_config |
|
|
|
MPV_BASE = ["mpv"] |
|
|
|
@ -555,11 +852,11 @@ class IPTVPlayer: |
|
|
|
self.loop.set_alarm_in(0, self.refresh_schedule) |
|
|
|
|
|
|
|
def refresh_schedule(self, loop=None, user_data=None): |
|
|
|
"""Update schedule information""" |
|
|
|
"""Update schedule information without animation for better performance""" |
|
|
|
try: |
|
|
|
print("[DEBUG] Refreshing schedule data...") |
|
|
|
debug_print("[DEBUG] Refreshing schedule data...") |
|
|
|
|
|
|
|
# Just update current shows without reloading everything |
|
|
|
# Update current shows without animations |
|
|
|
self.update_current_shows() |
|
|
|
self.last_update = datetime.now() |
|
|
|
|
|
|
|
@ -567,16 +864,13 @@ class IPTVPlayer: |
|
|
|
self.refresh_ui() |
|
|
|
self.update_footer() |
|
|
|
|
|
|
|
# Add notification to program view |
|
|
|
# Add simple update notification |
|
|
|
time_str = self.last_update.strftime("%H:%M:%S") |
|
|
|
notification = [ |
|
|
|
urwid.Text([("update", f"Schedule updated at {time_str}")]), |
|
|
|
urwid.Text("") # Empty line |
|
|
|
] |
|
|
|
# Prepend notification to existing content |
|
|
|
self.program_walker[:] = notification + self.program_walker[:] |
|
|
|
update_text = urwid.Text([("update", f"Schedule updated at {time_str}")]) |
|
|
|
empty_line = urwid.Text("") |
|
|
|
self.program_walker[:] = [update_text, empty_line] + self.program_walker[:] |
|
|
|
|
|
|
|
print("[DEBUG] Schedule refreshed successfully") |
|
|
|
debug_print("[DEBUG] Schedule refreshed successfully") |
|
|
|
except Exception as e: |
|
|
|
print(f"[ERROR] Failed to refresh schedule: {str(e)}") |
|
|
|
|
|
|
|
@ -606,15 +900,17 @@ class IPTVPlayer: |
|
|
|
self.on_channel_hover(button.channel) |
|
|
|
|
|
|
|
def on_channel_hover(self, channel): |
|
|
|
"""Update program info""" |
|
|
|
"""Update program info without animation for better performance""" |
|
|
|
self.current_channel = channel |
|
|
|
program_info = self.get_program_info(channel) |
|
|
|
# Convert each line to a Text widget |
|
|
|
|
|
|
|
# Direct update without animation for better performance |
|
|
|
self.program_walker[:] = [urwid.Text(line) for line in program_info] |
|
|
|
|
|
|
|
def on_channel_select(self, channel): |
|
|
|
"""Play channel when selected""" |
|
|
|
"""Play channel when selected (no animation for better performance)""" |
|
|
|
if channel.get('url'): |
|
|
|
# Play the channel directly without animation |
|
|
|
self.play_channel(channel['url']) |
|
|
|
|
|
|
|
def get_program_info(self, channel): |
|
|
|
@ -626,6 +922,14 @@ class IPTVPlayer: |
|
|
|
if channel.get('group'): |
|
|
|
info.append([("title", f"Group: {channel['group']}")]) |
|
|
|
|
|
|
|
# Debug program information |
|
|
|
if channel.get('current_show'): |
|
|
|
current_show = channel['current_show'] |
|
|
|
debug_print(f"[DEBUG] Program info - Title: {current_show.get('title')}") |
|
|
|
debug_print(f"[DEBUG] Program info - Season: {current_show.get('season')}") |
|
|
|
debug_print(f"[DEBUG] Program info - Episode: {current_show.get('episode')}") |
|
|
|
debug_print(f"[DEBUG] Program info - Air date: {current_show.get('air_date')}") |
|
|
|
|
|
|
|
if channel.get('current_show'): |
|
|
|
now = datetime.now() |
|
|
|
remaining = (channel['current_show']['stop'] - now).seconds // 60 |
|
|
|
@ -635,12 +939,57 @@ class IPTVPlayer: |
|
|
|
# Current show section with colorful formatting |
|
|
|
info.append([]) # Empty line |
|
|
|
info.append([("program_now", "--NOW PLAYING")]) |
|
|
|
info.append([("title", f"Title: {channel['current_show']['title']}")]) |
|
|
|
# Display title with season and episode prominently if available |
|
|
|
current_show = channel['current_show'] |
|
|
|
|
|
|
|
# Extract and display season/episode information |
|
|
|
season = current_show.get('season') |
|
|
|
episode = current_show.get('episode') |
|
|
|
|
|
|
|
# If season/episode not available in metadata, try to extract from title |
|
|
|
if season is None or episode is None: |
|
|
|
# Try SxxExx format (S01E05) |
|
|
|
se_match = re.search(r'S(\d+)E(\d+)', current_show['title'], re.IGNORECASE) |
|
|
|
if se_match: |
|
|
|
season = int(se_match.group(1)) |
|
|
|
episode = int(se_match.group(2)) |
|
|
|
debug_print(f"[DEBUG] Extracted S{season}E{episode} from title format") |
|
|
|
# Try season x episode format (1x05) |
|
|
|
elif 'x' in current_show['title'].lower(): |
|
|
|
x_match = re.search(r'(\d+)x(\d+)', current_show['title'], re.IGNORECASE) |
|
|
|
if x_match: |
|
|
|
season = int(x_match.group(1)) |
|
|
|
episode = int(x_match.group(2)) |
|
|
|
debug_print(f"[DEBUG] Extracted S{season}E{episode} from Nx format") |
|
|
|
|
|
|
|
# Display title with or without season/episode info |
|
|
|
if season is not None and episode is not None: |
|
|
|
# Format title with season/episode info |
|
|
|
formatted_title = f"Title: {current_show['title']} (S{season:02d}E{episode:02d})" |
|
|
|
info.append([("title", formatted_title)]) |
|
|
|
# Also add a detailed version on a separate line |
|
|
|
info.append([("program_desc", f"Season {season}, Episode {episode}")]) |
|
|
|
# Update the current_show object with the extracted info if it wasn't there already |
|
|
|
current_show['season'] = season |
|
|
|
current_show['episode'] = episode |
|
|
|
else: |
|
|
|
info.append([("title", f"Title: {current_show['title']}")]) |
|
|
|
|
|
|
|
# Add air date if available |
|
|
|
if current_show.get('air_date'): |
|
|
|
info.append([("time", f"Original Air Date: {current_show['air_date']}")]) |
|
|
|
|
|
|
|
info.append([ |
|
|
|
("time", f"Time: {start_time} - {end_time} "), |
|
|
|
("", f"({remaining} minutes remaining)") |
|
|
|
]) |
|
|
|
|
|
|
|
# Add IMDb link if we can generate one |
|
|
|
imdb_url = self.generate_imdb_url(current_show) |
|
|
|
# Skip IMDb URL display as it doesn't work well in Windows CMD |
|
|
|
# if imdb_url: |
|
|
|
# info.append([("update", f"IMDb: {imdb_url}")]) |
|
|
|
|
|
|
|
if channel['current_show']['desc']: |
|
|
|
info.append([]) # Empty line |
|
|
|
info.append([("program_desc", "--Description:")]) |
|
|
|
@ -658,9 +1007,53 @@ class IPTVPlayer: |
|
|
|
info.append([("divider", "─" * 50)]) |
|
|
|
info.append([]) # Empty line |
|
|
|
info.append([("program_next", "--UP NEXT")]) |
|
|
|
info.append([("title", f"Title: {channel['next_show']['title']}")]) |
|
|
|
|
|
|
|
# Display title with season and episode information |
|
|
|
next_show = channel['next_show'] |
|
|
|
|
|
|
|
# Extract season/episode information for next show |
|
|
|
season = next_show.get('season') |
|
|
|
episode = next_show.get('episode') |
|
|
|
|
|
|
|
# If season/episode not available in metadata, try to extract from title |
|
|
|
if season is None or episode is None: |
|
|
|
# Try SxxExx format (S01E05) |
|
|
|
se_match = re.search(r'S(\d+)E(\d+)', next_show['title'], re.IGNORECASE) |
|
|
|
if se_match: |
|
|
|
season = int(se_match.group(1)) |
|
|
|
episode = int(se_match.group(2)) |
|
|
|
debug_print(f"[DEBUG] Next show: Extracted S{season}E{episode} from title format") |
|
|
|
# Try season x episode format (1x05) |
|
|
|
elif 'x' in next_show['title'].lower(): |
|
|
|
x_match = re.search(r'(\d+)x(\d+)', next_show['title'], re.IGNORECASE) |
|
|
|
if x_match: |
|
|
|
season = int(x_match.group(1)) |
|
|
|
episode = int(x_match.group(2)) |
|
|
|
debug_print(f"[DEBUG] Next show: Extracted S{season}E{episode} from Nx format") |
|
|
|
|
|
|
|
# Display title with or without season/episode info |
|
|
|
if season is not None and episode is not None: |
|
|
|
# Format title with season/episode info |
|
|
|
formatted_title = f"Title: {next_show['title']} (S{season:02d}E{episode:02d})" |
|
|
|
info.append([("title", formatted_title)]) |
|
|
|
# Also add a detailed version on a separate line |
|
|
|
info.append([("program_desc", f"Season {season}, Episode {episode}")]) |
|
|
|
# Update the next_show object with the extracted info if it wasn't there already |
|
|
|
next_show['season'] = season |
|
|
|
next_show['episode'] = episode |
|
|
|
else: |
|
|
|
info.append([("title", f"Title: {next_show['title']}")]) |
|
|
|
|
|
|
|
# Add air date if available for next show |
|
|
|
if next_show.get('air_date'): |
|
|
|
info.append([("time", f"Original Air Date: {next_show['air_date']}")]) |
|
|
|
|
|
|
|
info.append([("time", f"Time: {next_start} - {next_end}")]) |
|
|
|
|
|
|
|
# Add IMDb link for next show if we can generate one |
|
|
|
imdb_url = self.generate_imdb_url(next_show) |
|
|
|
# Skip IMDb URL display as it doesn't work well in Windows CMD |
|
|
|
|
|
|
|
if channel['next_show']['desc']: |
|
|
|
info.append([]) # Empty line |
|
|
|
info.append([("program_desc", "--Description:")]) |
|
|
|
@ -682,9 +1075,53 @@ class IPTVPlayer: |
|
|
|
|
|
|
|
info.append([]) # Empty line |
|
|
|
info.append([("program_next", "--UP NEXT")]) |
|
|
|
info.append([("title", f"Title: {channel['next_show']['title']}")]) |
|
|
|
|
|
|
|
# Display title with season and episode information |
|
|
|
next_show = channel['next_show'] |
|
|
|
|
|
|
|
# Extract season/episode information for next show |
|
|
|
season = next_show.get('season') |
|
|
|
episode = next_show.get('episode') |
|
|
|
|
|
|
|
# If season/episode not available in metadata, try to extract from title |
|
|
|
if season is None or episode is None: |
|
|
|
# Try SxxExx format (S01E05) |
|
|
|
se_match = re.search(r'S(\d+)E(\d+)', next_show['title'], re.IGNORECASE) |
|
|
|
if se_match: |
|
|
|
season = int(se_match.group(1)) |
|
|
|
episode = int(se_match.group(2)) |
|
|
|
debug_print(f"[DEBUG] Next show: Extracted S{season}E{episode} from title format") |
|
|
|
# Try season x episode format (1x05) |
|
|
|
elif 'x' in next_show['title'].lower(): |
|
|
|
x_match = re.search(r'(\d+)x(\d+)', next_show['title'], re.IGNORECASE) |
|
|
|
if x_match: |
|
|
|
season = int(x_match.group(1)) |
|
|
|
episode = int(x_match.group(2)) |
|
|
|
debug_print(f"[DEBUG] Next show: Extracted S{season}E{episode} from Nx format") |
|
|
|
|
|
|
|
# Display title with or without season/episode info |
|
|
|
if season is not None and episode is not None: |
|
|
|
# Format title with season/episode info |
|
|
|
formatted_title = f"Title: {next_show['title']} (S{season:02d}E{episode:02d})" |
|
|
|
info.append([("title", formatted_title)]) |
|
|
|
# Also add a detailed version on a separate line |
|
|
|
info.append([("program_desc", f"Season {season}, Episode {episode}")]) |
|
|
|
# Update the next_show object with the extracted info if it wasn't there already |
|
|
|
next_show['season'] = season |
|
|
|
next_show['episode'] = episode |
|
|
|
else: |
|
|
|
info.append([("title", f"Title: {next_show['title']}")]) |
|
|
|
|
|
|
|
# Add air date if available for next show |
|
|
|
if next_show.get('air_date'): |
|
|
|
info.append([("time", f"Original Air Date: {next_show['air_date']}")]) |
|
|
|
|
|
|
|
info.append([("time", f"Time: {next_start} - {next_end}")]) |
|
|
|
|
|
|
|
# Add IMDb link for next show if we can generate one |
|
|
|
imdb_url = self.generate_imdb_url(next_show) |
|
|
|
# Skip IMDb URL display as it doesn't work well in Windows CMD |
|
|
|
|
|
|
|
if channel['next_show']['desc']: |
|
|
|
info.append([]) # Empty line |
|
|
|
info.append([("program_desc", "--Description:")]) |
|
|
|
@ -715,7 +1152,7 @@ class IPTVPlayer: |
|
|
|
self.program_walker[:] = [urwid.Text([("error", f"Failed to play stream: {str(e)}")])] |
|
|
|
|
|
|
|
def reload_data(self): |
|
|
|
"""Reload all data from sources without rebuilding UI""" |
|
|
|
"""Reload all data from sources without animation for better performance""" |
|
|
|
# Save current focus position |
|
|
|
current_focus_pos = self.channel_list.focus_position if self.channel_list.body else None |
|
|
|
|
|
|
|
@ -782,6 +1219,10 @@ class IPTVPlayer: |
|
|
|
self.start_update_thread() |
|
|
|
|
|
|
|
try: |
|
|
|
# Show splash screen animation before running the main loop |
|
|
|
self.display_splash_screen() |
|
|
|
|
|
|
|
# Start the main event loop |
|
|
|
self.loop.run() |
|
|
|
finally: |
|
|
|
# Clean up when exiting |
|
|
|
|