diff --git a/config/config.json b/config/config.json index a4380712..49cdf99e 100644 --- a/config/config.json +++ b/config/config.json @@ -63,7 +63,7 @@ } }, "clock": { - "enabled": true, + "enabled": false, "format": "%H:%M:%S", "update_interval": 1 }, @@ -98,7 +98,7 @@ "headlines_per_rotation": 2 }, "calendar": { - "enabled": true, + "enabled": false, "credentials_file": "credentials.json", "token_file": "token.pickle", "update_interval": 3600, @@ -106,7 +106,7 @@ "calendars": ["birthdays"] }, "nhl_scoreboard": { - "enabled": true, + "enabled": false, "test_mode": false, "update_interval_seconds": 300, "live_update_interval": 15, @@ -168,7 +168,7 @@ } }, "ncaam_scoreboard": { - "enabled": true, + "enabled": false, "test_mode": false, "update_interval_seconds": 3600, "live_update_interval": 15, @@ -182,7 +182,7 @@ } }, "ncaa_bb_scoreboard": { - "enabled": true, + "enabled": false, "test_mode": false, "update_interval_seconds": 3600, "live_update_interval": 20, @@ -202,7 +202,7 @@ "update_interval": 3600 }, "mlb": { - "enabled": true, + "enabled": false, "test_mode": false, "update_interval_seconds": 300, "live_update_interval": 20, @@ -220,10 +220,10 @@ "text_display": { "enabled": true, "text": "Subscribe to ChuckBuilds", - "font_path": "assets/fonts/5x7.bdf", - "font_size": 7, - "scroll": false, - "scroll_speed": 25, + "font_path": "assets/fonts/press-start-2p.ttf", + "font_size": 8, + "scroll": true, + "scroll_speed": 10, "text_color": [255, 0, 0], "background_color": [0, 0, 0] }, diff --git a/src/music_manager.py b/src/music_manager.py index 1ccad546..3b8de67e 100644 --- a/src/music_manager.py +++ b/src/music_manager.py @@ -48,6 +48,7 @@ class MusicManager: self.scroll_position_artist = 0 self.title_scroll_tick = 0 self.artist_scroll_tick = 0 + self.is_music_display_active = False # New state variable self._load_config() # Load config first self._initialize_clients() # Initialize based on loaded config @@ -115,11 +116,8 @@ class MusicManager: if self.preferred_source in ["auto", "ytm"]: try: self.ytm = YTMClient(update_callback=self._handle_ytm_direct_update) - if not self.ytm.is_available(): - logging.warning(f"YTM Companion server not reachable at {self.ytm.base_url}. YTM features disabled.") - self.ytm = None - else: - logging.info(f"YTM Companion server connected at {self.ytm.base_url}.") + # We no longer check is_available() or connect here. Connection is on-demand. + logging.info(f"YTMClient initialized. Connection will be managed on-demand. Configured URL: {self.ytm.base_url}") except Exception as e: logging.error(f"Failed to initialize YTM client: {e}") self.ytm = None @@ -127,11 +125,33 @@ class MusicManager: logging.info("YTM client initialization skipped due to preferred_source setting.") self.ytm = None + def activate_music_display(self): + logger.info("Music display activated.") + self.is_music_display_active = True + if self.ytm and self.preferred_source in ["auto", "ytm"]: + if not self.ytm.is_connected(): + logger.info("Attempting to connect YTM client due to music display activation.") + # Pass a reasonable timeout for on-demand connection + if self.ytm.connect_client(timeout=10): + logger.info("YTM client connected successfully on display activation.") + else: + logger.warning("YTM client failed to connect on display activation.") + else: + logger.debug("YTM client already connected during music display activation.") + + def deactivate_music_display(self): + logger.info("Music display deactivated.") + self.is_music_display_active = False + if self.ytm and self.ytm.is_connected(): + logger.info("Disconnecting YTM client due to music display deactivation.") + self.ytm.disconnect_client() + def _handle_ytm_direct_update(self, ytm_data): """Handles a direct state update from YTMClient.""" logger.debug(f"MusicManager received direct YTM update: {ytm_data.get('track', {}).get('title') if ytm_data else 'No Data'}") - if not self.enabled: + if not self.enabled or not self.is_music_display_active: # Check if display is active + logger.debug("Skipping YTM direct update: Manager disabled or music display not active.") return # Only process if YTM is the preferred source, or if auto and Spotify isn't actively playing. @@ -271,10 +291,11 @@ class MusicManager: if should_poll_ytm_now: # Re-check availability just before polling - if self.ytm.is_available(): + if self.ytm and self.ytm.is_connected(): # Check if connected instead of is_available() try: ytm_track = self.ytm.get_current_track() - if ytm_track and not ytm_track.get('player', {}).get('isPaused'): + # Ensure ytm_track is not None before trying to access its player info + if ytm_track and ytm_track.get('player') and not ytm_track.get('player', {}).get('isPaused'): # If YTM is preferred, it overrides Spotify even if Spotify was playing if self.preferred_source == "ytm" or not is_playing: polled_track_info = ytm_track @@ -282,11 +303,11 @@ class MusicManager: is_playing = True logging.debug(f"Polling YTM: Active track - {ytm_track.get('track', {}).get('title')}") else: - logging.debug("Polling YTM: No active track or player paused.") + logging.debug("Polling YTM: No active track or player paused (or track data missing player info).") except Exception as e: logging.error(f"Error polling YTM: {e}") else: - logging.debug("Skipping YTM poll: Server not available.") + logging.debug("Skipping YTM poll: Client not initialized or not connected.") # Consider setting self.ytm = None if it becomes unavailable repeatedly? # --- Consolidate and Check for Changes --- @@ -420,12 +441,16 @@ class MusicManager: else: logger.info("Music manager: Polling thread stopped.") self.poll_thread = None # Clear the thread object + # Also ensure YTM client is disconnected when polling stops completely + self.deactivate_music_display() # This will also handle YTM disconnect if needed # Method moved from DisplayController and renamed def display(self, force_clear: bool = False): if force_clear: # Removed self.force_clear as it's passed directly self.display_manager.clear() # self.force_clear = False # Not needed here + # If forcing clear, it implies the music display might be stopping or resetting. + self.deactivate_music_display() # Deactivate YTM if display is forced clear. # Use self.current_track_info which is updated by _poll_music_data display_info = self.current_track_info @@ -437,6 +462,9 @@ class MusicManager: logger.info("Music Screen (MusicManager): Nothing playing or info unavailable.") self._last_nothing_playing_log_time = time.time() + if self.is_music_display_active: # If we were showing music and now we are not + self.deactivate_music_display() + self.display_manager.clear() # Clear before drawing "Nothing Playing" text_width = self.display_manager.get_text_width("Nothing Playing", self.display_manager.regular_font) x_pos = (self.display_manager.matrix.width - text_width) // 2 @@ -451,6 +479,11 @@ class MusicManager: self.last_album_art_url = None # Also clear the URL return + # If we've reached here, it means we are about to display actual music info. + # Ensure YTM is active if it's a potential source. + if not self.is_music_display_active: + self.activate_music_display() + # Ensure screen is cleared if not force_clear but needed (e.g. transition from "Nothing Playing") # This might be handled by DisplayController's force_clear logic, but can be an internal check too. # For now, assuming DisplayController manages the initial clear for a new mode. diff --git a/src/ytm_client.py b/src/ytm_client.py index 83afbf28..f77d3a48 100644 --- a/src/ytm_client.py +++ b/src/ytm_client.py @@ -114,75 +114,71 @@ class YTMClient: else: logging.warning(f"YTM auth file not found at {YTM_AUTH_CONFIG_PATH}. Run the authentication script to generate it. YTM features will be disabled.") - def _ensure_connected(self, timeout=5): + def connect_client(self, timeout=10): if not self.ytm_token: - # No token, so cannot authenticate or connect. Log this clearly. - # load_config already warns if token file or token itself is missing. - # logging.warning("No YTM token loaded. Cannot connect to Socket.IO. Run authentication script.") + logging.warning("No YTM token loaded. Cannot connect to Socket.IO. Run authentication script.") self.is_connected = False return False - if not self.is_connected: - logging.info(f"Attempting to connect to YTM Socket.IO server: {self.base_url} on namespace /api/v1/realtime") - auth_payload = {"token": self.ytm_token} + if self.is_connected: + logging.debug("YTM client already connected.") + return True - try: - self._connection_event.clear() # Clear event before attempting connection - self.sio.connect( - self.base_url, - transports=['websocket'], - wait_timeout=timeout, - namespaces=['/api/v1/realtime'], - auth=auth_payload - ) - # self._connection_event.clear() # No longer clear here - # Use a slightly longer timeout for the event wait than the connect call itself - # to ensure the connect event has time to be processed. - event_wait_timeout = timeout + 5 # e.g., if connect timeout is 10s, wait 15s for the event - if not self._connection_event.wait(timeout=event_wait_timeout): - logging.warning(f"YTM Socket.IO connection event not received within {event_wait_timeout}s (connect timeout was {timeout}s).") - self.is_connected = False # Ensure is_connected is false on timeout - return False - return self.is_connected # This should be true if connect event fired and no timeout - except socketio.exceptions.ConnectionError as e: - logging.error(f"YTM Socket.IO connection error: {e}") + logging.info(f"Attempting to connect to YTM Socket.IO server: {self.base_url} on namespace /api/v1/realtime") + auth_payload = {"token": self.ytm_token} + + try: + self._connection_event.clear() + self.sio.connect( + self.base_url, + transports=['websocket'], + wait_timeout=timeout, + namespaces=['/api/v1/realtime'], + auth=auth_payload + ) + event_wait_timeout = timeout + 5 + if not self._connection_event.wait(timeout=event_wait_timeout): + logging.warning(f"YTM Socket.IO connection event not received within {event_wait_timeout}s (connect timeout was {timeout}s).") self.is_connected = False return False - except Exception as e: - logging.error(f"Unexpected error during YTM Socket.IO connection: {e}") - self.is_connected = False - return False - return True # Already connected + logging.info(f"YTM Socket.IO connection successful: {self.is_connected}") + return self.is_connected + except socketio.exceptions.ConnectionError as e: + logging.error(f"YTM Socket.IO connection error: {e}") + self.is_connected = False + return False + except Exception as e: + logging.error(f"Unexpected error during YTM Socket.IO connection: {e}") + self.is_connected = False + return False def is_available(self): - if not self.ytm_token: # Quick check: if no token, definitely not available. + if not self.ytm_token: return False - if not self.is_connected: - return self._ensure_connected(timeout=10) - return True + return self.is_connected def get_current_track(self): - if not self.is_available(): # is_available will attempt to connect if not connected and token exists - # logging.warning("YTM client not available, cannot get current track.") # is_available() or _ensure_connected() already logs issues. + if not self.is_connected: return None with self._data_lock: if self.last_known_track_data: return self.last_known_track_data else: - # This is a normal state if no music is playing or just connected - # logging.debug("No track data received yet from YTM Companion Socket.IO.") return None def disconnect_client(self): if self.is_connected: self.sio.disconnect() logging.info("YTM Socket.IO client disconnected.") + self.is_connected = False + else: + logging.debug("YTM Socket.IO client already disconnected or not connected.") # Example Usage (for testing - needs to be adapted for Socket.IO async nature) # if __name__ == '__main__': # client = YTMClient() -# if client.is_available(): +# if client.connect_client(): # print("YTM Server is available (Socket.IO).") # try: # for _ in range(10): # Poll for a few seconds