Fix API to use correct Moonlight endpoint

- POST /api/video/play with JSON {"nfc_id": "card_id"}
- Matches web_interface.py api_play_video() endpoint
- Proper JSON Content-Type header
- Better error handling and response parsing
- Added skip video support
- Version bump to 1.3.0
This commit is contained in:
2026-02-22 12:57:32 +11:00
parent a904f89d1d
commit 8a9faac09c

View File

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