diff --git a/web_server.py b/web_server.py index 8f5e6a08..280135f4 100644 --- a/web_server.py +++ b/web_server.py @@ -119,41 +119,192 @@ def run_service_test(service, test_config): def run_detection(server_type): """ - Performs network detection for a given server type (plex, jellyfin, slskd). - This is a blocking function that scans the network. + Performs comprehensive network detection for a given server type (plex, jellyfin, slskd). + This implements the same scanning logic as the GUI's detection threads. """ - # This is a simplified version of the logic in your QThreads. - # A full implementation would be more extensive. - # For demonstration, we'll check common local addresses. - print(f"Running detection for {server_type}...") - common_ips = ["localhost", "127.0.0.1"] - ports = { - 'plex': 32400, - 'jellyfin': 8096, - 'slskd': 5030 - } - port = ports.get(server_type) - if not port: + print(f"Running comprehensive detection for {server_type}...") + + def get_network_info(): + """Get comprehensive network information with subnet detection""" + try: + # Get local IP using socket method + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + local_ip = s.getsockname()[0] + s.close() + + # Try to get actual subnet mask + try: + if platform.system() == "Windows": + # Windows: Use netsh to get subnet info + result = subprocess.run(['netsh', 'interface', 'ip', 'show', 'config'], + capture_output=True, text=True, timeout=3) + # Parse output for subnet mask (simplified) + subnet_mask = "255.255.255.0" # Default fallback + else: + # Linux/Mac: Try to parse network interfaces + result = subprocess.run(['ip', 'route', 'show'], + capture_output=True, text=True, timeout=3) + subnet_mask = "255.255.255.0" # Default fallback + except: + subnet_mask = "255.255.255.0" # Default /24 + + # Calculate network range + network = ipaddress.IPv4Network(f"{local_ip}/{subnet_mask}", strict=False) + return str(network.network_address), str(network.netmask), local_ip, network + + except Exception as e: + # Fallback to original method + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + local_ip = s.getsockname()[0] + s.close() + + # Default to /24 network + network = ipaddress.IPv4Network(f"{local_ip}/24", strict=False) + return str(network.network_address), "255.255.255.0", local_ip, network + + def test_plex_server(ip, port=32400): + """Test if a Plex server is running at the given IP and port""" + try: + url = f"http://{ip}:{port}/web/index.html" + response = requests.get(url, timeout=2, allow_redirects=True) + + # Check for Plex-specific indicators + if response.status_code == 200: + # Check if it's actually Plex + if 'plex' in response.text.lower() or 'X-Plex' in str(response.headers): + return f"http://{ip}:{port}" + + # Also try the API endpoint + api_url = f"http://{ip}:{port}/identity" + api_response = requests.get(api_url, timeout=1) + if api_response.status_code == 200 and 'MediaContainer' in api_response.text: + return f"http://{ip}:{port}" + + except: + pass return None - for ip in common_ips: - url = f"http://{ip}:{port}" + def test_jellyfin_server(ip, port=8096): + """Test if a Jellyfin server is running at the given IP and port""" try: - if server_type == 'slskd': - # slskd check is different - response = requests.get(f"{url}/api/v0/session", timeout=1) - if response.status_code in [200, 401]: - print(f"Found {server_type} at {url}") - return url - else: - response = requests.get(url, timeout=1) - # A simple 200 OK is a good sign - if response.status_code == 200: - print(f"Found {server_type} at {url}") - return url - except requests.RequestException: - continue - return None + # Try the system info endpoint first + url = f"http://{ip}:{port}/System/Info" + response = requests.get(url, timeout=2, allow_redirects=True) + + if response.status_code == 200: + # Check if response contains Jellyfin-specific content + if 'jellyfin' in response.text.lower() or 'ServerName' in response.text: + return f"http://{ip}:{port}" + + # Also try the web interface + web_url = f"http://{ip}:{port}/web/index.html" + web_response = requests.get(web_url, timeout=1) + if web_response.status_code == 200 and 'jellyfin' in web_response.text.lower(): + return f"http://{ip}:{port}" + + except: + pass + return None + + def test_slskd_server(ip, port=5030): + """Test if a slskd server is running at the given IP and port""" + try: + # slskd specific API endpoint + url = f"http://{ip}:{port}/api/v0/session" + response = requests.get(url, timeout=2) + + # slskd returns 401 when not authenticated, which is still a valid response + if response.status_code in [200, 401]: + return f"http://{ip}:{port}" + + except: + pass + return None + + try: + network_addr, netmask, local_ip, network = get_network_info() + + # Select the appropriate test function + test_functions = { + 'plex': test_plex_server, + 'jellyfin': test_jellyfin_server, + 'slskd': test_slskd_server + } + + test_func = test_functions.get(server_type) + if not test_func: + return None + + # Priority 1: Test localhost first + print(f"Testing localhost for {server_type}...") + localhost_result = test_func("localhost") + if localhost_result: + print(f"Found {server_type} at localhost!") + return localhost_result + + # Priority 2: Test local IP + print(f"Testing local IP {local_ip} for {server_type}...") + local_result = test_func(local_ip) + if local_result: + print(f"Found {server_type} at {local_ip}!") + return local_result + + # Priority 3: Test common IPs (router gateway, etc.) + common_ips = [ + local_ip.rsplit('.', 1)[0] + '.1', # Typical gateway + local_ip.rsplit('.', 1)[0] + '.2', # Alternative gateway + local_ip.rsplit('.', 1)[0] + '.100', # Common static IP + ] + + print(f"Testing common IPs for {server_type}...") + for ip in common_ips: + print(f" Checking {ip}...") + result = test_func(ip) + if result: + print(f"Found {server_type} at {ip}!") + return result + + # Priority 4: Scan the network range (limited to reasonable size) + network_hosts = list(network.hosts()) + if len(network_hosts) > 50: + # Limit scan to reasonable size for performance + step = max(1, len(network_hosts) // 50) + network_hosts = network_hosts[::step] + + print(f"Scanning network range for {server_type} ({len(network_hosts)} hosts)...") + + # Use ThreadPoolExecutor for concurrent scanning (limited for web context) + with ThreadPoolExecutor(max_workers=5) as executor: + # Submit all tasks + future_to_ip = {executor.submit(test_func, str(ip)): str(ip) + for ip in network_hosts} + + try: + for future in as_completed(future_to_ip): + ip = future_to_ip[future] + try: + result = future.result() + if result: + print(f"Found {server_type} at {ip}!") + # Cancel all pending futures before returning + for f in future_to_ip: + if not f.done(): + f.cancel() + return result + except Exception as e: + print(f"Error testing {ip}: {e}") + continue + except Exception as e: + print(f"Error in concurrent scanning: {e}") + + print(f"No {server_type} server found on network") + return None + + except Exception as e: + print(f"Error during {server_type} detection: {e}") + return None # --- Web UI Routes --- @@ -395,6 +546,59 @@ def stream_stop(): # Placeholder: simulates stopping a stream return jsonify({"success": True}) +@app.route('/api/version-info', methods=['GET']) +def get_version_info(): + """ + Returns version information and release notes, matching the GUI's VersionInfoModal content. + This provides the same data that the GUI version modal displays. + """ + version_data = { + "version": "0.65", + "title": "What's New in SoulSync", + "subtitle": "Version 0.65 - Tidal Playlist Integration", + "sections": [ + { + "title": "🎵 Complete Tidal Playlist Integration", + "description": "Full Tidal playlist support with seamless workflow integration matching YouTube/Spotify functionality", + "features": [ + "• Native Tidal API client with OAuth 2.0 authentication and automatic token management", + "• Tidal playlist tab positioned between Spotify and YouTube with identical UI/UX patterns", + "• Advanced playlist card system with persistent state tracking across all phases", + "• Complete discovery workflow: discovering → discovered → syncing → downloading phases", + "• Intelligent track matching using existing Spotify-based algorithms for compatibility", + "• Smart modal routing with proper state persistence (close/cancel behavior)", + "• Full refresh functionality with comprehensive worker cleanup and modal management" + ], + "usage_note": "Configure Tidal in Settings → Connections, then discover and sync your Tidal playlists just like Spotify!" + }, + { + "title": "⚙️ Advanced Workflow Features", + "description": "Sophisticated state management and user experience improvements", + "features": [ + "• Identical workflow behavior across all playlist sources (Spotify, YouTube, Tidal)", + "• Smart refresh system that cancels all active operations and preserves playlist names", + "• Phase-aware card clicking: routes to discovery, sync progress, or download modals appropriately", + "• Proper modal state persistence: closing download modals preserves discovery state", + "• Cancel operations reset playlists to fresh state for updated playlist data", + "• Multi-server compatibility: works with both Plex and Jellyfin automatically" + ] + }, + { + "title": "🔧 Technical Implementation Details", + "description": "Robust architecture ensuring reliable playlist management across all sources", + "features": [ + "• Implemented comprehensive state tracking system with playlist card hub architecture", + "• Added PKCE (Proof Key for Code Exchange) OAuth flow for enhanced Tidal security", + "• Created unified modal system supporting YouTube, Spotify, and Tidal workflows", + "• Enhanced worker cancellation system for proper resource cleanup during operations", + "• JSON:API response parsing for Tidal's complex relationship-based data structure", + "• Future-ready architecture for additional music streaming service integrations" + ] + } + ] + } + return jsonify(version_data) + # --- Main Execution --- diff --git a/webui/index.html b/webui/index.html index 07238a85..fbdfcde1 100644 --- a/webui/index.html +++ b/webui/index.html @@ -463,6 +463,29 @@
+ + +