diff --git a/terminalTv.py b/terminalTv.py index 23c8420..ca6b86e 100644 --- a/terminalTv.py +++ b/terminalTv.py @@ -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