From 5a4e82ab6f73b0bc46f9ba7659e21bd94f4dfd9d Mon Sep 17 00:00:00 2001 From: ChuckBuilds <33324927+ChuckBuilds@users.noreply.github.com> Date: Wed, 7 May 2025 20:25:28 -0500 Subject: [PATCH] Refactor YTMClient to use Socket.IO for companion server communication --- requirements.txt | 8 ++- src/ytm_client.py | 135 +++++++++++++++++++++++++++++++--------------- 2 files changed, 99 insertions(+), 44 deletions(-) diff --git a/requirements.txt b/requirements.txt index 53e58aa6..76361df1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,10 @@ google-auth-oauthlib==1.0.0 google-auth-httplib2==0.1.0 google-api-python-client==2.86.0 freetype-py==2.5.1 -spotipy \ No newline at end of file +spotipy +Flask +unidecode +icalevents +python-socketio +python-engineio +websockets \ No newline at end of file diff --git a/src/ytm_client.py b/src/ytm_client.py index 514549c8..0496cf92 100644 --- a/src/ytm_client.py +++ b/src/ytm_client.py @@ -1,7 +1,9 @@ -import requests +import socketio import logging import json import os +import time +import threading logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') @@ -13,6 +15,34 @@ class YTMClient: def __init__(self): self.base_url = None self.load_config() + self.sio = socketio.Client(logger=True, engineio_logger=False) + self.last_known_track_data = None + self.is_connected = False + self._data_lock = threading.Lock() + self._connection_event = threading.Event() + + @self.sio.event + def connect(): + logging.info(f"Successfully connected to YTM Companion Socket.IO server at {self.base_url}") + self.is_connected = True + self._connection_event.set() + + @self.sio.event + def connect_error(data): + logging.error(f"YTM Companion Socket.IO connection failed: {data}") + self.is_connected = False + self._connection_event.set() + + @self.sio.event + def disconnect(): + logging.info(f"Disconnected from YTM Companion Socket.IO server at {self.base_url}") + self.is_connected = False + + @self.sio.on('ytm_track_update') + def on_track_update(data): + logging.debug(f"Received track update from YTM Companion: {data}") + with self._data_lock: + self.last_known_track_data = data def load_config(self): default_url = "http://localhost:9863" @@ -37,53 +67,72 @@ class YTMClient: logging.error(f"Error loading YTM config: {e}") self.base_url = default_url logging.info(f"YTM Companion URL set to: {self.base_url}") + if self.base_url and self.base_url.startswith("ws://"): + self.base_url = "http://" + self.base_url[5:] + elif self.base_url and self.base_url.startswith("wss://"): + self.base_url = "https://" + self.base_url[6:] - def _make_request(self, endpoint): - """Helper method to make requests to the companion server.""" - if not self.base_url: - logging.error("YTM base URL not configured.") - return None - try: - url = f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}" - response = requests.get(url, timeout=1) # Short timeout - response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx) - return response.json() - except requests.exceptions.ConnectionError: - # This is expected if the server isn't running - logging.debug(f"Could not connect to YTM Companion server at {self.base_url}") - return None - except requests.exceptions.Timeout: - logging.warning(f"Timeout connecting to YTM Companion server at {self.base_url}") - return None - except requests.exceptions.RequestException as e: - logging.error(f"Error requesting {endpoint} from YTM: {e}") - return None + def _ensure_connected(self, timeout=5): + if not self.is_connected: + logging.info(f"Attempting to connect to YTM Socket.IO server: {self.base_url}") + try: + self.sio.connect(self.base_url, transports=['websocket'], wait_timeout=timeout) + self._connection_event.clear() + if not self._connection_event.wait(timeout=timeout): + logging.warning(f"YTM Socket.IO connection attempt timed out after {timeout}s.") + return False + 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 + return True def is_available(self): - """Checks if the YTM companion server is reachable.""" - # Use a lightweight endpoint if available, otherwise try main query - # For now, just try the main query endpoint - return self._make_request('/query') is not None + if not self.is_connected: + return self._ensure_connected(timeout=2) + return True def get_current_track(self): - """Fetches the currently playing track from the YTM companion server.""" - data = self._make_request('/query') - # Add more specific error handling or data validation if needed - if data and 'track' in data and 'player' in data: - return data - else: - logging.debug("Received no or incomplete data from YTM /query") + if not self._ensure_connected(): + logging.warning("YTM client not connected, cannot get current track.") return None -# Example Usage (for testing) + with self._data_lock: + if self.last_known_track_data: + return self.last_known_track_data + else: + 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.") + +# Example Usage (for testing - needs to be adapted for Socket.IO async nature) # if __name__ == '__main__': -# client = YTMClient() -# if client.is_available(): -# print("YTM Server is available.") -# track = client.get_current_track() -# if track: -# print(json.dumps(track, indent=2)) -# else: -# print("No track currently playing or error fetching.") -# else: -# print(f"YTM Server not available at {client.base_url}. Is YTMD running with companion server enabled?") \ No newline at end of file +# client = YTMClient() +# if client.connect_client(): # Assuming connect_client is the new public method to initiate +# print("YTM Server is available (Socket.IO).") +# try: +# for _ in range(10): # Poll for a few seconds +# track = client.get_current_track() +# if track: +# print(json.dumps(track, indent=2)) +# else: +# print("No track currently playing or error fetching (Socket.IO).") +# time.sleep(2) +# finally: +# client.disconnect_client() +# else: +# print(f"YTM Server not available at {client.base_url} (Socket.IO). Is YTMD running with companion server enabled?") + +# It's important to note that a long-running application would typically +# keep the socketio client running in a background thread if sio.wait() is used, +# or integrate with an asyncio event loop. The above __main__ is simplified. +# The MusicManager's polling thread will interact with this client. \ No newline at end of file