Files
moonlight-drive-in/web_interface.py
jessikitty 05a8bd237b Upload files to "/"
Add core Python modules
2025-12-09 16:49:13 +11:00

912 lines
34 KiB
Python

# web_interface_clean.py
"""
Clean Web Interface - No Logs, Fixed Status Updates
Focuses on controls and statistics with better real-time updates
"""
import os
import json
import logging
import threading
import time
from datetime import datetime, timedelta
from pathlib import Path
from flask import Flask, render_template, request, jsonify
from flask_socketio import SocketIO, emit
import queue
class DateTimeJSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
elif isinstance(obj, timedelta):
return str(obj)
return super().default(obj)
def make_json_safe(data):
if isinstance(data, dict):
return {key: make_json_safe(value) for key, value in data.items()}
elif isinstance(data, list):
return [make_json_safe(item) for item in data]
elif isinstance(data, datetime):
return data.isoformat()
elif isinstance(data, timedelta):
return str(data)
else:
return data
class WebInterface:
def __init__(self, debug_console=None, host='0.0.0.0', port=8547):
self.debug_console = debug_console
self.video_player = None
self.host = host
self.port = port
self.logger = logging.getLogger(__name__)
# Web command tracking
self.command_history = []
self.max_history = 100
self.command_lock = threading.Lock()
# Statistics cache with shorter refresh
self.stats_cache = {}
self.last_stats_update = 0
self.stats_cache_duration = 0.5 # Update every 500ms for better responsiveness
# Message queues
self.message_queue = queue.Queue()
self.log_queue = queue.Queue()
# Flask app setup
self.app = Flask(__name__)
self.app.config['SECRET_KEY'] = 'video_player_secret_key_2024'
# SocketIO setup
self.socketio = SocketIO(
self.app,
cors_allowed_origins="*",
json=DateTimeJSONEncoder,
async_mode='threading'
)
# Background task control
self.running = True
self.stats_thread = None
self.log_thread = None
# Setup routes and handlers
self.setup_routes()
self.setup_socketio_handlers()
self.logger.info("Clean web interface initialized successfully")
# =================================================================
# REQUIRED COMPATIBILITY METHODS
# =================================================================
def log(self, message, level="INFO"):
"""Required by existing debug console"""
try:
if level == "ERROR":
self.logger.error(message)
elif level == "WARNING":
self.logger.warning(message)
else:
self.logger.info(message)
except Exception as e:
logging.getLogger(__name__).error(f"Web interface log error: {e}")
def log_error(self, message):
self.log(message, "ERROR")
def log_info(self, message):
self.log(message, "INFO")
def log_warning(self, message):
self.log(message, "WARNING")
def log_video_played(self, video_name):
"""Required compatibility method"""
self.log(f"Video played: {video_name}", "INFO")
self.log_web_command("VIDEO_PLAYED",
params={'video': video_name},
source="player",
details=f"Video started: {video_name}")
# Force immediate stats update when video changes
self.last_stats_update = 0
self.stats_cache = {}
def set_video_player(self, video_player):
"""Required by main application"""
self.video_player = video_player
self.log(f"Video player connected to web interface")
def update_global_input_status(self, *args, **kwargs):
"""Required by debug console - flexible parameters"""
pass
def get_stats(self):
"""Alternative method name for getting statistics"""
return self.get_current_stats()
# =================================================================
# CORE METHODS - IMPROVED STATUS TRACKING
# =================================================================
def log_web_command(self, command, params=None, source="web", success=True, details=None):
"""Comprehensive logging for all web commands"""
try:
timestamp = datetime.now()
log_entry = {
'timestamp': timestamp.isoformat(),
'command': command,
'source': source,
'success': success,
'params': params or {},
'details': details or 'Command executed'
}
with self.command_lock:
self.command_history.append(log_entry)
if len(self.command_history) > self.max_history:
self.command_history.pop(0)
status = "SUCCESS" if success else "FAILED"
param_str = f" | Params: {params}" if params else ""
detail_str = f" | {details}" if details else ""
log_message = f"[WEB-CMD] {command} [{status}] from {source.upper()}{param_str}{detail_str}"
self.logger.info(log_message)
except Exception as e:
self.logger.error(f"Error logging web command: {e}")
def get_current_stats(self):
"""Get current statistics with improved real-time updates"""
try:
current_time = time.time()
# Use cached stats if very recent
if (current_time - self.last_stats_update) < self.stats_cache_duration and self.stats_cache:
return self.stats_cache
# Get fresh stats from debug console
raw_stats = {}
if self.debug_console:
try:
if hasattr(self.debug_console, 'get_stats'):
raw_stats = self.debug_console.get_stats()
elif hasattr(self.debug_console, 'stats'):
raw_stats = dict(self.debug_console.stats)
except Exception as e:
self.logger.debug(f"Error getting debug console stats: {e}")
# Calculate uptime
uptime_str = '00:00:00'
if 'start_time' in raw_stats and isinstance(raw_stats['start_time'], datetime):
uptime = datetime.now() - raw_stats['start_time']
uptime_str = str(uptime).split('.')[0]
# Get current status with better detection
current_status = self.get_current_status()
current_video = self.get_current_video()
# Build stats object
safe_stats = {
'videos_played': raw_stats.get('videos_played', 0),
'nfc_scans': raw_stats.get('key_presses', 0),
'errors': raw_stats.get('errors', 0),
'uptime': uptime_str,
'queue_depth': raw_stats.get('queue_depth', 0),
'current_video': current_video,
'status': current_status,
'fullscreen': raw_stats.get('fullscreen', True),
'web_commands_executed': len(self.command_history),
'last_update': datetime.now().isoformat(),
'connection_status': 'Connected' if self.debug_console else 'No Player',
'is_playing': self.is_video_playing(),
'player_ready': bool(self.get_video_player())
}
# Cache the stats
self.stats_cache = safe_stats
self.last_stats_update = current_time
return safe_stats
except Exception as e:
self.logger.error(f"Error getting stats: {e}")
return {
'videos_played': 0,
'nfc_scans': 0,
'errors': 1,
'uptime': '00:00:00',
'queue_depth': 0,
'current_video': 'Error',
'status': 'Error getting stats',
'fullscreen': True,
'web_commands_executed': 0,
'last_update': datetime.now().isoformat(),
'connection_status': 'Error',
'is_playing': False,
'player_ready': False
}
def get_current_status(self):
"""Get current status with better detection"""
try:
player = self.get_video_player()
if not player:
return "No video player connected"
# Check if specific video is playing
if hasattr(player, 'specific_video_playing') and player.specific_video_playing:
return "Playing specific video"
# Check if any video is playing
if hasattr(player, 'is_video_playing'):
try:
if player.is_video_playing():
return "Playing trailer"
else:
return "Ready"
except:
return "Ready"
# Check if video player is running
if hasattr(player, 'running') and player.running:
return "Ready"
return "Player stopped"
except Exception as e:
self.logger.debug(f"Error getting status: {e}")
return "Status unknown"
def get_current_video(self):
"""Get current video with better detection"""
try:
player = self.get_video_player()
if not player:
return "No video player"
# Try to get current video path
if hasattr(player, 'current_video_path') and player.current_video_path:
video_path = Path(player.current_video_path)
return video_path.name
# Try to get from stats
if self.debug_console and hasattr(self.debug_console, 'stats'):
current_video = self.debug_console.stats.get('current_video', 'None')
if current_video and current_video != 'None':
return current_video
return "No video"
except Exception as e:
self.logger.debug(f"Error getting current video: {e}")
return "Unknown"
def is_video_playing(self):
"""Check if video is currently playing"""
try:
player = self.get_video_player()
if not player:
return False
if hasattr(player, 'is_video_playing'):
return player.is_video_playing()
return False
except Exception as e:
self.logger.debug(f"Error checking if video playing: {e}")
return False
def get_video_player(self):
"""Get video player from multiple possible sources"""
if self.video_player:
return self.video_player
elif self.debug_console and hasattr(self.debug_console, 'video_player'):
return self.debug_console.video_player
else:
return None
def broadcast_stats(self):
"""Broadcast statistics update via SocketIO"""
try:
safe_stats = self.get_current_stats()
if hasattr(self, 'socketio'):
self.socketio.emit('stats_update', safe_stats)
except Exception as e:
self.logger.error(f"Failed to broadcast stats: {e}")
def stats_broadcast_loop(self):
"""Background thread to broadcast stats updates - faster updates"""
while self.running:
try:
self.broadcast_stats()
time.sleep(1) # Update every 1 second for better responsiveness
except Exception as e:
self.logger.error(f"Stats broadcast loop error: {e}")
time.sleep(5)
def log_broadcast_loop(self):
"""Background thread - not needed for clean interface but kept for compatibility"""
while self.running:
try:
# Clear the queue but don't broadcast logs
while not self.log_queue.empty():
try:
self.log_queue.get_nowait()
except queue.Empty:
break
time.sleep(1)
except Exception as e:
self.logger.error(f"Log broadcast loop error: {e}")
time.sleep(1)
def setup_routes(self):
"""Setup Flask routes"""
@self.app.route('/')
def index():
"""Main interface"""
try:
self.log_web_command("PAGE_LOAD", source="web", details="Main interface accessed")
return self.create_clean_html()
except Exception as e:
self.log_web_command("PAGE_LOAD", success=False, details=f"Error: {e}")
return self.create_clean_html()
@self.app.route('/api/video/skip', methods=['POST'])
def api_skip_video():
"""Skip current video"""
try:
self.log_web_command("SKIP_VIDEO", source="web", details="User clicked skip button")
if self.get_video_player():
self.get_video_player().skip_current_video()
self.log_web_command("SKIP_VIDEO", success=True, details="Skip command sent to player")
# Force immediate stats update
self.last_stats_update = 0
return jsonify({'success': True, 'message': 'Video skipped successfully'})
else:
self.log_web_command("SKIP_VIDEO", success=False, details="No video player available")
return jsonify({'success': False, 'message': 'Video player not available'}), 500
except Exception as e:
self.log_web_command("SKIP_VIDEO", success=False, details=f"Error: {str(e)}")
return jsonify({'success': False, 'message': f'Skip failed: {str(e)}'}), 500
@self.app.route('/api/video/toggle-fullscreen', methods=['POST'])
def api_toggle_fullscreen():
"""Toggle fullscreen"""
try:
self.log_web_command("TOGGLE_FULLSCREEN", source="web", details="User toggled fullscreen")
if self.get_video_player():
self.get_video_player().toggle_fullscreen()
self.log_web_command("TOGGLE_FULLSCREEN", success=True, details="Fullscreen toggle sent to player")
return jsonify({'success': True, 'message': 'Fullscreen toggled'})
else:
self.log_web_command("TOGGLE_FULLSCREEN", success=False, details="No video player available")
return jsonify({'success': False, 'message': 'Video player not available'}), 500
except Exception as e:
self.log_web_command("TOGGLE_FULLSCREEN", success=False, details=f"Error: {str(e)}")
return jsonify({'success': False, 'message': f'Fullscreen toggle failed: {str(e)}'}), 500
@self.app.route('/api/nfc/send', methods=['POST'])
def api_send_nfc():
"""Send NFC input"""
try:
data = request.get_json()
nfc_id = str(data.get('nfc_id', '')).strip()
self.log_web_command("NFC_INPUT", params={'nfc_id': nfc_id},
source="web", details=f"User sent NFC ID: {nfc_id}")
if self.get_video_player():
self.get_video_player().play_specific_video(nfc_id)
self.log_web_command("NFC_INPUT", params={'nfc_id': nfc_id},
success=True, details=f"NFC {nfc_id} sent to player")
# Force immediate stats update
self.last_stats_update = 0
return jsonify({'success': True, 'message': f'NFC {nfc_id} sent successfully'})
else:
self.log_web_command("NFC_INPUT", success=False, details="No video player available")
return jsonify({'success': False, 'message': 'Video player not available'}), 500
except Exception as e:
self.log_web_command("NFC_INPUT", params={'nfc_id': nfc_id if 'nfc_id' in locals() else 'unknown'},
success=False, details=f"Error: {str(e)}")
return jsonify({'success': False, 'message': f'NFC input failed: {str(e)}'}), 500
@self.app.route('/api/stats', methods=['GET'])
def api_get_stats():
"""Get statistics"""
try:
stats = self.get_current_stats()
return jsonify(stats)
except Exception as e:
self.logger.error(f"Stats API error: {e}")
return jsonify({'error': 'Stats unavailable'}), 500
def setup_socketio_handlers(self):
"""Setup SocketIO event handlers"""
@self.socketio.on('connect')
def handle_connect():
"""Handle client connection"""
self.logger.info("Web client connected")
self.log_web_command("CONNECT", source="socketio", details="Client connected")
# Send initial stats immediately
emit('stats_update', self.get_current_stats())
@self.socketio.on('disconnect')
def handle_disconnect():
"""Handle client disconnection"""
self.logger.info("Web client disconnected")
self.log_web_command("DISCONNECT", source="socketio", details="Client disconnected")
def create_clean_html(self):
"""Create clean HTML interface without logs"""
return '''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Video Player Controller</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: white;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.panel {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 25px;
margin: 20px 0;
border-radius: 15px;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.btn {
background: linear-gradient(45deg, #4CAF50, #45a049);
color: white;
border: none;
padding: 12px 24px;
margin: 8px;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(76, 175, 80, 0.4);
}
.btn:active {
transform: translateY(0);
}
.status {
padding: 15px;
border-radius: 10px;
margin: 15px 0;
text-align: center;
font-weight: 600;
font-size: 18px;
transition: all 0.3s ease;
}
.status.success {
background: linear-gradient(45deg, #4CAF50, #45a049);
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3);
}
.status.error {
background: linear-gradient(45deg, #f44336, #d32f2f);
box-shadow: 0 4px 15px rgba(244, 67, 54, 0.3);
}
.status.info {
background: linear-gradient(45deg, #2196F3, #1976D2);
box-shadow: 0 4px 15px rgba(33, 150, 243, 0.3);
}
.input-group {
display: flex;
margin: 15px 0;
gap: 10px;
}
.input-group input {
flex: 1;
padding: 12px 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 8px;
background: rgba(255, 255, 255, 0.1);
color: white;
font-size: 16px;
backdrop-filter: blur(10px);
}
.input-group input::placeholder {
color: rgba(255, 255, 255, 0.7);
}
.input-group input:focus {
outline: none;
border-color: #4CAF50;
box-shadow: 0 0 10px rgba(76, 175, 80, 0.3);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin: 20px 0;
}
.stat {
background: rgba(255, 255, 255, 0.15);
padding: 20px;
border-radius: 12px;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.2);
transition: transform 0.3s ease;
}
.stat:hover {
transform: translateY(-3px);
}
.stat-value {
font-size: 2.2rem;
font-weight: bold;
color: #4CAF50;
margin-bottom: 8px;
text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
}
.stat-label {
font-size: 0.9rem;
opacity: 0.9;
text-transform: uppercase;
letter-spacing: 1px;
}
.current-video {
background: rgba(255, 255, 255, 0.15);
padding: 20px;
border-radius: 12px;
margin: 20px 0;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.current-video-value {
font-size: 1.3rem;
font-weight: 600;
color: #FFD700;
margin-top: 10px;
word-break: break-word;
}
.connection-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
animation: pulse 2s infinite;
}
.connected { background: #4CAF50; }
.disconnected { background: #f44336; }
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.controls-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
h3 {
margin-bottom: 20px;
font-size: 1.4rem;
color: #FFD700;
text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
}
@media (max-width: 768px) {
.container { padding: 10px; }
.panel { padding: 15px; }
.header h1 { font-size: 2rem; }
.stats-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎬 Video Player Controller</h1>
<div id="status" class="status info">
<span id="connectionIndicator" class="connection-indicator disconnected"></span>
Connecting to video player...
</div>
</div>
<div class="panel">
<h3>📊 Live Statistics</h3>
<div class="stats-grid">
<div class="stat">
<div class="stat-value" id="videosPlayed">0</div>
<div class="stat-label">Videos Played</div>
</div>
<div class="stat">
<div class="stat-value" id="nfcScans">0</div>
<div class="stat-label">NFC Scans</div>
</div>
<div class="stat">
<div class="stat-value" id="uptime">00:00:00</div>
<div class="stat-label">Uptime</div>
</div>
<div class="stat">
<div class="stat-value" id="playerStatus">Unknown</div>
<div class="stat-label">Player Status</div>
</div>
</div>
<div class="current-video">
<h3>🎥 Currently Playing</h3>
<div class="current-video-value" id="currentVideo">No video</div>
</div>
</div>
<div class="panel">
<h3>🎮 Video Controls</h3>
<div class="controls-grid">
<button class="btn" onclick="skipVideo()">⏭️ Skip Video</button>
<button class="btn" onclick="toggleFullscreen()">🔳 Toggle Fullscreen</button>
</div>
</div>
<div class="panel">
<h3>🏷️ NFC Input</h3>
<div class="input-group">
<input type="text" id="nfcInput" placeholder="Enter NFC ID and press Enter..." onkeypress="handleNFCKeypress(event)">
<button class="btn" onclick="sendNFC()">Send NFC</button>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.0/socket.io.js"></script>
<script>
const socket = io();
let lastStatsUpdate = 0;
function showStatus(message, type = 'info', duration = 3000) {
const status = document.getElementById('status');
const indicator = document.getElementById('connectionIndicator');
status.innerHTML = `<span id="connectionIndicator" class="connection-indicator ${type === 'success' ? 'connected' : 'disconnected'}"></span>${message}`;
status.className = `status ${type}`;
if (duration > 0) {
setTimeout(() => {
status.innerHTML = '<span id="connectionIndicator" class="connection-indicator connected"></span>Ready';
status.className = 'status success';
}, duration);
}
}
function skipVideo() {
fetch('/api/video/skip', {method: 'POST'})
.then(r => r.json())
.then(data => {
showStatus(data.message, data.success ? 'success' : 'error');
if (data.success) {
// Request immediate stats update
requestStatsUpdate();
}
})
.catch(e => showStatus('Network error', 'error'));
}
function toggleFullscreen() {
fetch('/api/video/toggle-fullscreen', {method: 'POST'})
.then(r => r.json())
.then(data => showStatus(data.message, data.success ? 'success' : 'error'))
.catch(e => showStatus('Network error', 'error'));
}
function sendNFC() {
const nfcId = document.getElementById('nfcInput').value.trim();
if (!nfcId) return showStatus('Please enter NFC ID', 'error');
fetch('/api/nfc/send', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({nfc_id: nfcId})
})
.then(r => r.json())
.then(data => {
showStatus(data.message, data.success ? 'success' : 'error');
if (data.success) {
document.getElementById('nfcInput').value = '';
// Request immediate stats update
requestStatsUpdate();
}
})
.catch(e => showStatus('Network error', 'error'));
}
function handleNFCKeypress(event) {
if (event.key === 'Enter') sendNFC();
}
function requestStatsUpdate() {
// Request fresh stats from server
fetch('/api/stats')
.then(r => r.json())
.then(updateStats)
.catch(e => console.error('Stats request failed:', e));
}
function updateStats(stats) {
try {
// Update all stat values
document.getElementById('videosPlayed').textContent = stats.videos_played || 0;
document.getElementById('nfcScans').textContent = stats.nfc_scans || 0;
document.getElementById('uptime').textContent = stats.uptime || '00:00:00';
document.getElementById('playerStatus').textContent = stats.status || 'Unknown';
// Update current video with better formatting
const currentVideo = stats.current_video || 'No video';
const videoElement = document.getElementById('currentVideo');
if (currentVideo.length > 50) {
videoElement.textContent = currentVideo.substring(0, 47) + '...';
videoElement.title = currentVideo;
} else {
videoElement.textContent = currentVideo;
videoElement.title = currentVideo;
}
// Update connection status
const isConnected = stats.player_ready && stats.connection_status === 'Connected';
const indicator = document.getElementById('connectionIndicator');
if (indicator) {
indicator.className = `connection-indicator ${isConnected ? 'connected' : 'disconnected'}`;
}
lastStatsUpdate = Date.now();
} catch (e) {
console.error('Error updating stats:', e);
}
}
// Socket event handlers
socket.on('connect', () => {
showStatus('Connected to video player', 'success', 0);
console.log('Connected to video player web interface');
requestStatsUpdate();
});
socket.on('disconnect', () => {
showStatus('Disconnected from video player', 'error', 0);
console.log('Disconnected from video player');
});
socket.on('stats_update', updateStats);
// Request stats update every 5 seconds as backup
setInterval(() => {
if (Date.now() - lastStatsUpdate > 5000) {
requestStatsUpdate();
}
}, 5000);
// Initial connection attempt
showStatus('Attempting to connect...', 'info');
</script>
</body>
</html>'''
def run(self, host=None, port=None, debug=False, **kwargs):
"""Fixed run method that accepts all possible parameters"""
try:
run_host = host or self.host
run_port = port or self.port
self.logger.info(f"Starting clean web interface on {run_host}:{run_port}")
# Start background threads
if not self.stats_thread or not self.stats_thread.is_alive():
self.stats_thread = threading.Thread(target=self.stats_broadcast_loop, daemon=True)
self.log_thread = threading.Thread(target=self.log_broadcast_loop, daemon=True)
self.stats_thread.start()
self.log_thread.start()
self.logger.info("Background threads started")
# Start the SocketIO server
self.socketio.run(
self.app,
host=run_host,
port=run_port,
debug=False,
use_reloader=False,
allow_unsafe_werkzeug=True
)
except Exception as e:
self.logger.error(f"Web interface startup error: {e}")
self.logger.info("Web interface failed to start but application can continue")
def stop(self):
"""Stop the web interface"""
self.logger.info("Stopping clean web interface...")
self.running = False
if self.stats_thread and self.stats_thread.is_alive():
self.stats_thread.join(timeout=2)
if self.log_thread and self.log_thread.is_alive():
self.log_thread.join(timeout=2)
self.logger.info("Clean web interface stopped")
# Helper functions for backward compatibility
def create_web_interface(debug_console=None, host='0.0.0.0', port=8547):
return WebInterface(debug_console, host, port)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
web = WebInterface()
try:
web.run()
except KeyboardInterrupt:
web.stop()