diff --git a/moonlight_integration.py b/moonlight_integration.py index 219ef9e..6f591cb 100644 --- a/moonlight_integration.py +++ b/moonlight_integration.py @@ -6,12 +6,12 @@ Integrates the LEGO Dimensions portal reader with the Moonlight Drive-In video player system using NFC card IDs (10-digit decimal format). When a LEGO Dimensions disc is placed on the portal, it sends the -10-digit card ID as plain text to the player (like keyboard input). +card ID to the player's API to trigger video playback. -v1.2.0 Changes: -- Sends 10-digit card ID as plain text (like keyboard entry) -- No JSON, just the raw card ID string -- Updated default API URL +v1.3.0 Changes: +- Uses correct API endpoint: POST /api/video/play +- Sends JSON body: {"nfc_id": "card_id"} +- Compatible with Moonlight web_interface.py """ import sys @@ -27,7 +27,7 @@ from lego_dimensions_reader import ( COLORS ) -__version__ = "1.2.0" +__version__ = "1.3.0" class MoonlightDimensionsClient: @@ -35,8 +35,8 @@ class MoonlightDimensionsClient: Integration client connecting LEGO Dimensions portal to Moonlight Drive-In video player. - Sends 10-digit NFC card ID as plain text to the player, - simulating keyboard input. + Sends NFC card ID to /api/video/play endpoint to trigger + video playback based on the player's mapping configuration. """ def __init__(self, api_url: str, mapping_file: Optional[str] = None): @@ -45,12 +45,13 @@ class MoonlightDimensionsClient: Args: api_url: Base URL for Moonlight Drive-In API (e.g., "http://100.94.163.117:8547") - mapping_file: Optional JSON file mapping card IDs to video paths (for local display only) + mapping_file: Optional JSON file for local display of mappings """ self.api_url = api_url.rstrip('/') self.reader = LegoDimensionsReader() - # Optional local mapping for display purposes + # Optional local mapping for display purposes only + # The actual video mappings are configured on the player side self.video_mapping: Dict[str, str] = {} if mapping_file: self._load_mapping(mapping_file) @@ -73,7 +74,7 @@ class MoonlightDimensionsClient: raw_mappings = data.get('mappings', data) for key, value in raw_mappings.items(): self.video_mapping[str(key)] = value - print(f"Loaded {len(self.video_mapping)} video mappings") + print(f"Loaded {len(self.video_mapping)} local video mappings") except FileNotFoundError: pass # Mapping file is optional except json.JSONDecodeError as e: @@ -98,85 +99,102 @@ class MoonlightDimensionsClient: def _on_tag_insert(self, tag: TagInfo): """Handle tag placement - send card ID to player.""" - # Get 10-digit zero-padded card ID - card_id = tag.nfc_card_id_str # "0983187584" + # Get the card ID (use the decimal format the player expects) + card_id = str(tag.nfc_card_id) # e.g., "983187584" + card_id_padded = tag.nfc_card_id_str # e.g., "0983187584" (10 digits) print(f"\n{'='*50}") print(f"✓ TAG DETECTED on {tag.pad.name} pad") print(f" UID: {tag.uid_hex}") print(f" Card ID: {card_id}") + print(f" Card ID (padded): {card_id_padded}") - # Send card ID to player - self._send_card_id(card_id) + # Send card ID to player API + success = self._play_video(card_id) - # Set pad to green to indicate sent - self.reader.set_pad_color(tag.pad, COLORS['GREEN']) + if success: + # Set pad to green to indicate success + self.reader.set_pad_color(tag.pad, COLORS['GREEN']) + else: + # Flash red to indicate error + self.reader.flash_pad(tag.pad, COLORS['RED'], count=3) - # Show mapped video if we have local mapping - video_path = self.video_mapping.get(str(tag.nfc_card_id)) or \ - self.video_mapping.get(card_id) + # Show local mapping info if available + video_path = self.video_mapping.get(card_id) or \ + self.video_mapping.get(card_id_padded) if video_path: - print(f" Mapped to: {video_path}") + print(f" Local mapping: {video_path}") print(f"{'='*50}") def _on_tag_remove(self, tag: TagInfo): """Handle tag removal.""" - card_id = tag.nfc_card_id_str + card_id = str(tag.nfc_card_id) print(f"\n✗ TAG REMOVED from {tag.pad.name} pad") print(f" Card ID: {card_id}") - # Optionally send stop command - self._send_stop() - # Turn off pad LED self.reader.set_pad_color(tag.pad, COLORS['OFF']) self._current_card_id = None - def _send_card_id(self, card_id: str): - """Send the 10-digit card ID as plain text to the player.""" + def _play_video(self, card_id: str) -> bool: + """ + Send play command to Moonlight API. + + Uses POST /api/video/play with JSON body {"nfc_id": "card_id"} + + Args: + card_id: The NFC card ID (decimal string) + + Returns: + True if successful, False otherwise + """ try: - # Send as plain text - like keyboard input response = requests.post( - f"{self.api_url}/api/input", - data=card_id, - headers={'Content-Type': 'text/plain'}, + f"{self.api_url}/api/video/play", + json={"nfc_id": card_id}, + headers={'Content-Type': 'application/json'}, timeout=5 ) if response.ok: - self._current_card_id = card_id - print(f" ▶ Sent: {card_id}") - else: - # Try alternate endpoint - response = requests.post( - f"{self.api_url}/api/play", - data=card_id, - headers={'Content-Type': 'text/plain'}, - timeout=5 - ) - if response.ok: + result = response.json() + if result.get('success'): self._current_card_id = card_id - print(f" ▶ Sent: {card_id}") + print(f" ▶ Playing video for card {card_id}") + return True else: - print(f" ⚠ API Error: {response.status_code}") + message = result.get('message', 'Unknown error') + print(f" ⚠ Player error: {message}") + return False + else: + print(f" ⚠ API Error: {response.status_code}") + return False except requests.exceptions.ConnectionError: print(f" ⚠ Cannot connect to player at {self.api_url}") + return False except requests.exceptions.Timeout: print(f" ⚠ Request timed out") + return False + except Exception as e: + print(f" ⚠ Error: {e}") + return False - def _send_stop(self): - """Send stop command to player.""" + def _skip_video(self) -> bool: + """Send skip command to player.""" try: - requests.post( - f"{self.api_url}/api/stop", + response = requests.post( + f"{self.api_url}/api/video/skip", timeout=5 ) - print(f" ⏹ Stop sent") + if response.ok: + print(f" ⏭ Video skipped") + return True except requests.exceptions.RequestException: - pass # Ignore stop errors + pass + return False def start(self): """Start the integration client.""" @@ -185,7 +203,8 @@ class MoonlightDimensionsClient: print(f" Integration v{__version__}") print("="*50) print(f"\nPlayer URL: {self.api_url}") - print("Sending 10-digit card IDs as text") + print(f"API Endpoint: POST /api/video/play") + print(f"Request Format: {{\"nfc_id\": \"card_id\"}}") print("\nStarting portal connection...") self.reader.start() @@ -200,7 +219,12 @@ def main(): """Main entry point.""" # Configuration - UPDATE THIS TO YOUR PLAYER IP MOONLIGHT_API = "http://100.94.163.117:8547" - MAPPING_FILE = "video_mappings.json" # Optional + MAPPING_FILE = "video_mappings.json" # Optional local reference + + # Allow command line override of API URL + if len(sys.argv) > 1: + MOONLIGHT_API = sys.argv[1] + print(f"Using API URL from command line: {MOONLIGHT_API}") client = MoonlightDimensionsClient( api_url=MOONLIGHT_API,