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:
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user