mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
194 lines
8.4 KiB
Python
194 lines
8.4 KiB
Python
import socketio
|
|
import logging
|
|
import json
|
|
import os
|
|
import time
|
|
import threading
|
|
|
|
# Ensure application-level logging is configured (as it is)
|
|
# logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
|
|
# Reduce verbosity of socketio and engineio libraries
|
|
logging.getLogger('socketio.client').setLevel(logging.WARNING)
|
|
logging.getLogger('socketio.server').setLevel(logging.WARNING)
|
|
logging.getLogger('engineio.client').setLevel(logging.WARNING)
|
|
logging.getLogger('engineio.server').setLevel(logging.WARNING)
|
|
|
|
# Define paths relative to this file's location
|
|
CONFIG_DIR = os.path.join(os.path.dirname(__file__), '..', 'config')
|
|
CONFIG_PATH = os.path.join(CONFIG_DIR, 'config.json')
|
|
# Resolve to an absolute path
|
|
CONFIG_PATH = os.path.abspath(CONFIG_PATH)
|
|
|
|
# Path for the separate YTM authentication token file
|
|
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, update_callback=None):
|
|
self.base_url = None
|
|
self.ytm_token = None
|
|
self.load_config() # Loads URL and token
|
|
self.sio = socketio.Client(logger=False, engineio_logger=False)
|
|
self.last_known_track_data = None
|
|
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():
|
|
logging.info(f"Successfully connected to YTM Companion Socket.IO server at {self.base_url} on namespace /api/v1/realtime")
|
|
self.is_connected = True
|
|
self._connection_event.set()
|
|
|
|
@self.sio.event(namespace='/api/v1/realtime')
|
|
def connect_error(data):
|
|
logging.error(f"YTM Companion Socket.IO connection failed for namespace /api/v1/realtime: {data}")
|
|
self.is_connected = False
|
|
self._connection_event.set()
|
|
|
|
@self.sio.event(namespace='/api/v1/realtime')
|
|
def disconnect():
|
|
logging.info(f"Disconnected from YTM Companion Socket.IO server at {self.base_url} on namespace /api/v1/realtime")
|
|
self.is_connected = False
|
|
|
|
@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:
|
|
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"
|
|
self.base_url = default_url # Start with default
|
|
|
|
# Load base_url from main config.json
|
|
if not os.path.exists(CONFIG_PATH):
|
|
logging.warning(f"Main config file not found at {CONFIG_PATH}. Using default YTM URL: {self.base_url}")
|
|
else:
|
|
try:
|
|
with open(CONFIG_PATH, 'r') as f:
|
|
loaded_config = json.load(f)
|
|
music_config = loaded_config.get("music", {})
|
|
self.base_url = music_config.get("YTM_COMPANION_URL", default_url)
|
|
if not self.base_url:
|
|
logging.warning("YTM_COMPANION_URL missing or empty in config.json music section, using default.")
|
|
self.base_url = default_url
|
|
except json.JSONDecodeError:
|
|
logging.error(f"Error decoding JSON from main config {CONFIG_PATH}. Using default YTM URL.")
|
|
except Exception as e:
|
|
logging.error(f"Error loading YTM_COMPANION_URL from main config {CONFIG_PATH}: {e}. Using default YTM 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:]
|
|
|
|
# Load ytm_token from ytm_auth.json
|
|
self.ytm_token = None # Reset token before trying to load
|
|
if os.path.exists(YTM_AUTH_CONFIG_PATH):
|
|
try:
|
|
with open(YTM_AUTH_CONFIG_PATH, 'r') as f:
|
|
auth_data = json.load(f)
|
|
self.ytm_token = auth_data.get("YTM_COMPANION_TOKEN")
|
|
if self.ytm_token:
|
|
logging.info(f"YTM Companion token loaded from {YTM_AUTH_CONFIG_PATH}.")
|
|
else:
|
|
logging.warning(f"YTM_COMPANION_TOKEN not found in {YTM_AUTH_CONFIG_PATH}. YTM features will be disabled until token is present.")
|
|
except json.JSONDecodeError:
|
|
logging.error(f"Error decoding JSON from YTM auth file {YTM_AUTH_CONFIG_PATH}. YTM features will be disabled.")
|
|
except Exception as e:
|
|
logging.error(f"Error loading YTM auth config {YTM_AUTH_CONFIG_PATH}: {e}. YTM features will be disabled.")
|
|
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 connect_client(self, timeout=10):
|
|
if not self.ytm_token:
|
|
logging.warning("No YTM token loaded. Cannot connect to Socket.IO. Run authentication script.")
|
|
self.is_connected = False
|
|
return False
|
|
|
|
if self.is_connected:
|
|
logging.debug("YTM client already connected.")
|
|
return True
|
|
|
|
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
|
|
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:
|
|
return False
|
|
return self.is_connected
|
|
|
|
def get_current_track(self):
|
|
if not self.is_connected:
|
|
return None
|
|
|
|
with self._data_lock:
|
|
if self.last_known_track_data:
|
|
return self.last_known_track_data
|
|
else:
|
|
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.connect_client():
|
|
# 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 and token generated?") |