From 89ac66ec9ddff20d0ee61922863a8b546bb10fa0 Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Fri, 23 May 2025 14:21:27 -0500 Subject: [PATCH] better ytm api integration for faster metadata updates --- src/music_manager.py | 69 +++++++++++++++++++++++++++++++++++++++++++- src/ytm_client.py | 14 +++++++-- 2 files changed, 80 insertions(+), 3 deletions(-) diff --git a/src/music_manager.py b/src/music_manager.py index c897100a..d6652d22 100644 --- a/src/music_manager.py +++ b/src/music_manager.py @@ -119,7 +119,7 @@ class MusicManager: # Initialize YTM Client if needed if self.preferred_source in ["auto", "ytm"]: try: - self.ytm = YTMClient() + 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 @@ -132,6 +132,73 @@ class MusicManager: logging.info("YTM client initialization skipped due to preferred_source setting.") self.ytm = None + 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: + return + + # Only process if YTM is the preferred source, or if auto and Spotify isn't actively playing. + # This check is to ensure we don't override an active Spotify session if preferred_source is 'auto' + # and Spotify just happens to be paused but YTM starts playing something. + # The main polling loop has more robust logic for who 'wins' in auto mode. + # This direct callback should primarily act when YTM is the clear choice or nothing else is playing. + + spotify_is_playing = False + if self.current_source == MusicSource.SPOTIFY and self.current_track_info and self.current_track_info.get('is_playing'): + spotify_is_playing = True + + if not (self.preferred_source == "ytm" or (self.preferred_source == "auto" and not spotify_is_playing)): + logger.debug("Skipping YTM direct update due to preferred_source/Spotify state.") + return + + # Check if player is actually playing (not paused, not an ad) + player_info = ytm_data.get('player', {}) + video_info = ytm_data.get('video', {}) + is_actually_playing_ytm = (player_info.get('trackState') == 1) and not player_info.get('adPlaying', False) + + if not ytm_data or not is_actually_playing_ytm: + # If YTM is not playing or data is null, and we were on YTM, treat as a stop. + if self.current_source == MusicSource.YTM: + logger.info("YTM direct update indicates YTM stopped. Clearing YTM info.") + simplified_info = self.get_simplified_track_info(None, MusicSource.NONE) + polled_source = MusicSource.NONE # Effectively, nothing is playing from YTM's perspective + else: + # Not currently on YTM, and YTM is not playing, so no change to announce from YTM's side. + return + else: + simplified_info = self.get_simplified_track_info(ytm_data, MusicSource.YTM) + polled_source = MusicSource.YTM + + has_changed = False + if simplified_info != self.current_track_info: + has_changed = True + + old_album_art_url = self.current_track_info.get('album_art_url') if self.current_track_info else None + new_album_art_url = simplified_info.get('album_art_url') if simplified_info else None + + self.current_track_info = simplified_info + # Only set current_source to YTM if YTM is actually playing and preferred or auto + self.current_source = polled_source if is_actually_playing_ytm and polled_source == MusicSource.YTM else self.current_source + if not is_actually_playing_ytm and self.current_source == MusicSource.YTM: + self.current_source = MusicSource.NONE # If YTM stopped, it's no longer the source + + if new_album_art_url != old_album_art_url: + self.album_art_image = None + self.last_album_art_url = new_album_art_url + + display_title = self.current_track_info.get('title', 'None') if self.current_track_info else 'None' + logger.info(f"YTM Direct Update: Track change detected. Source: {self.current_source.name}. Track: {display_title}") + else: + logger.debug("YTM Direct Update: No change in simplified track info.") + + if has_changed and self.update_callback: + try: + self.update_callback(self.current_track_info) # This is the callback to DisplayController + except Exception as e: + logger.error(f"Error executing DisplayController update callback from YTM direct update: {e}") + def _fetch_and_resize_image(self, url: str, target_size: tuple[int, int]) -> Image.Image | None: """Fetches an image from a URL, resizes it, and returns a PIL Image object.""" if not url: diff --git a/src/ytm_client.py b/src/ytm_client.py index 5ad2f6f9..83afbf28 100644 --- a/src/ytm_client.py +++ b/src/ytm_client.py @@ -25,7 +25,7 @@ YTM_AUTH_CONFIG_PATH = os.path.join(CONFIG_DIR, 'ytm_auth.json') YTM_AUTH_CONFIG_PATH = os.path.abspath(YTM_AUTH_CONFIG_PATH) class YTMClient: - def __init__(self): + def __init__(self, update_callback=None): self.base_url = None self.ytm_token = None self.load_config() # Loads URL and token @@ -34,6 +34,7 @@ class YTMClient: self.is_connected = False self._data_lock = threading.Lock() self._connection_event = threading.Event() + self.external_update_callback = update_callback @self.sio.event(namespace='/api/v1/realtime') def connect(): @@ -55,8 +56,17 @@ class YTMClient: @self.sio.on('state-update', namespace='/api/v1/realtime') def on_state_update(data): logging.debug(f"Received state update from YTM Companion on /api/v1/realtime: {data}") + new_data_received = False with self._data_lock: - self.last_known_track_data = data + if self.last_known_track_data != data: + self.last_known_track_data = data + new_data_received = True + + if new_data_received and self.external_update_callback: + try: + self.external_update_callback(data) + except Exception as cb_ex: + logging.error(f"Error executing YTMClient external_update_callback: {cb_ex}") def load_config(self): default_url = "http://localhost:9863"