#!/usr/bin/env python3 """ Smart Auto-Connect NFC Network Client Moonlight Drive-In Theater System Enhanced client with smart DNS resolution and automatic connection. Compatible with NFCNetworkClient v1.1.0 (global input capture). """ import tkinter as tk from tkinter import ttk import sys import os import socket import subprocess import time from typing import Optional, List, Tuple # Import the base client try: from nfc_network_client import NFCNetworkClient, CLIENT_VERSION as BASE_VERSION except ImportError: print("ERROR: Cannot find nfc_network_client.py") print("Make sure nfc_network_client.py is in the same directory") sys.exit(1) SMART_CLIENT_VERSION = "2.0.0" class DNSResolver: """Smart DNS resolution with multiple fallback methods""" def __init__(self, device_name: str, fallback_ip: str, port: int = 8547): self.device_name = device_name self.fallback_ip = fallback_ip self.port = port self.last_known_ip: Optional[str] = None def resolve(self) -> Tuple[Optional[str], str]: """ Try multiple DNS resolution methods Returns: (ip_address, method_name) """ print(f"\nAttempting to locate device: {self.device_name}") # Method 1: Standard DNS result = self._try_standard_dns() if result: return result, "STANDARD_DNS" # Method 2: mDNS (.local) result = self._try_mdns() if result: return result, "MDNS" # Method 3: Tailscale DNS result = self._try_tailscale_dns() if result: return result, "TAILSCALE_DNS" # Method 4: Fallback to known IP if self.fallback_ip: print(f"\nMethod: Fallback to known IP") if self._check_host_reachable(self.fallback_ip): print(f"SUCCESS: Fallback IP is reachable: {self.fallback_ip}") return self.fallback_ip, "FALLBACK_IP" print("\nERROR: Could not locate device using any method") return None, "FAILED" def _try_standard_dns(self) -> Optional[str]: """Try standard DNS resolution""" print(f"\nMethod: Standard DNS") try: ip = socket.gethostbyname(self.device_name) print(f" [DNS] ✓ Resolved: {self.device_name} -> {ip}") if self._check_host_reachable(ip): print(f"SUCCESS: Found {self.device_name} at {ip} via Standard DNS") return ip except socket.gaierror: print(f" [DNS] ✗ Failed to resolve: {self.device_name}") return None def _try_mdns(self) -> Optional[str]: """Try mDNS resolution (.local)""" mdns_name = f"{self.device_name}.local" print(f"\nMethod: mDNS (Bonjour)") try: ip = socket.gethostbyname(mdns_name) print(f" [mDNS] ✓ Resolved: {mdns_name} -> {ip}") if self._check_host_reachable(ip): print(f"SUCCESS: Found {self.device_name} at {ip} via mDNS") return ip except socket.gaierror: print(f" [mDNS] ✗ Failed to resolve: {mdns_name}") return None def _try_tailscale_dns(self) -> Optional[str]: """Try Tailscale DNS resolution""" ts_name = f"{self.device_name}.tail-scale.ts.net" print(f"\nMethod: Tailscale VPN DNS") try: ip = socket.gethostbyname(ts_name) print(f" [Tailscale] ✓ Resolved: {ts_name} -> {ip}") if self._check_host_reachable(ip): print(f"SUCCESS: Found {self.device_name} at {ip} via Tailscale") return ip except socket.gaierror: print(f" [Tailscale] ✗ Failed to resolve: {ts_name}") return None def _check_host_reachable(self, ip: str) -> bool: """Check if host is reachable via ping""" try: # Try quick ping (1 packet, 1 second timeout) if sys.platform == "win32": result = subprocess.run( ["ping", "-n", "1", "-w", "1000", ip], capture_output=True, timeout=2 ) else: result = subprocess.run( ["ping", "-c", "1", "-W", "1", ip], capture_output=True, timeout=2 ) if result.returncode == 0: print(f" [Ping] ✓ Host reachable: {ip}") return True else: print(f" [Ping] ✗ Host unreachable: {ip}") return False except (subprocess.TimeoutExpired, Exception) as e: print(f" [Ping] ✗ Ping failed: {e}") return False class SmartAutoConnectClient(NFCNetworkClient): """Enhanced NFC client with smart auto-connect""" def __init__(self, root: tk.Tk, device_name: str, fallback_ip: str, port: int = 8547): # Store smart connect settings self.device_name = device_name self.fallback_ip = fallback_ip self.target_port = port self.resolver = DNSResolver(device_name, fallback_ip, port) # Initialize parent with root super().__init__(root) # Update title to show smart connect self.root.title(f"NFC Network Client - Smart Connect v{SMART_CLIENT_VERSION}") # Customize UI for smart connect self._customize_ui() def _customize_ui(self): """Add smart connect specific UI elements""" # Update server entry to show device name self.server_entry.delete(0, tk.END) self.server_entry.insert(0, f"{self.device_name} (auto-resolved)") self.server_entry.config(state="disabled") def connect_to_server(self): """Override connect to use smart DNS resolution""" self.log(f"Smart connecting to: {self.device_name}") self.update_status("Resolving...", "orange") self.connect_btn.config(state="disabled") # Resolve in background thread def resolve_and_connect(): ip, method = self.resolver.resolve() if ip: print(f"\n{'='*70}") print(f"WILL CONNECT TO: {ip}:{self.target_port}") print(f"Resolution method: {method}") print(f"{'='*70}\n") # Try to connect url = f"http://{ip}:{self.target_port}" # Test the connection import requests try: response = requests.get( f"{url}/api/status", timeout=2 ) if response.status_code == 200: data = response.json() if data.get("server_type") == "moonlight_drivein": # Success! self.root.after(0, lambda: self.connection_success(url)) return except: pass # Failed self.root.after(0, self.connection_failed) import threading threading.Thread(target=resolve_and_connect, daemon=True).start() def connection_failed(self): """Handle connection failure with smart reconnect""" super().connection_failed() self.log("Will retry in 5 seconds...", "INFO") # Auto-retry after 5 seconds self.root.after(5000, self.connect_to_server) def print_banner(): """Print startup banner""" print("\n" + "="*70) print(" " * 20 + "NFC Network Client - Smart Auto-Connect") print("="*70) print(f"\nTarget device: {CONFIG['device_name']}") print(f"Fallback IP: {CONFIG['fallback_ip']}") print(f"Port: {CONFIG['port']}") print("\n" + "="*70) def load_config() -> dict: """Load configuration from environment or defaults""" return { 'device_name': os.environ.get('DRIVEIN_DEVICE', 'drive-in'), 'fallback_ip': os.environ.get('DRIVEIN_IP', '100.94.163.117'), 'port': int(os.environ.get('DRIVEIN_PORT', '8547')) } # Global config CONFIG = load_config() def main(): """Main entry point""" print(f"\nNFC Network Client - Smart Auto-Connect v{SMART_CLIENT_VERSION}") print(f"Based on NFC Network Client v{BASE_VERSION}") print("="*70) print_banner() # Create Tkinter root root = tk.Tk() try: # Create and run smart client app = SmartAutoConnectClient( root, device_name=CONFIG['device_name'], fallback_ip=CONFIG['fallback_ip'], port=CONFIG['port'] ) print("Client started successfully!") print("Window should appear shortly...") print("="*70 + "\n") root.mainloop() except Exception as e: print(f"\nERROR: Client failed to start") print(f"Error: {e}") import traceback traceback.print_exc() return 1 finally: if hasattr(app, 'keyboard_listener') and app.keyboard_listener: app.keyboard_listener.stop() return 0 if __name__ == "__main__": sys.exit(main())