auto detect plex

pull/8/head
Broque Thomas 9 months ago
parent 845f8e995f
commit cecbd539a6

@ -5,6 +5,180 @@ from PyQt6.QtCore import Qt, QThread, pyqtSignal
from PyQt6.QtGui import QFont
from config.settings import config_manager
class PlexDetectionThread(QThread):
progress_updated = pyqtSignal(int, str) # progress value, current url
detection_completed = pyqtSignal(str) # found_url (empty if not found)
def __init__(self):
super().__init__()
self.cancelled = False
def cancel(self):
self.cancelled = True
def run(self):
import requests
import socket
import ipaddress
import subprocess
import platform
from concurrent.futures import ThreadPoolExecutor, as_completed
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
try:
network_addr, netmask, local_ip, network = get_network_info()
# Build list of IPs to test
test_ips = []
# Priority 1: Test localhost first
if not self.cancelled:
self.progress_updated.emit(5, "http://localhost:32400")
localhost_result = test_plex_server("localhost")
if localhost_result:
self.detection_completed.emit(localhost_result)
return
# Priority 2: Test local IP
if not self.cancelled:
self.progress_updated.emit(10, f"http://{local_ip}:32400")
local_result = test_plex_server(local_ip)
if local_result:
self.detection_completed.emit(local_result)
return
# 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
]
progress = 15
for ip in common_ips:
if self.cancelled:
break
self.progress_updated.emit(progress, f"http://{ip}:32400")
result = test_plex_server(ip)
if result:
self.detection_completed.emit(result)
return
progress += 5
# 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]
progress_step = max(1, (85 - progress) // len(network_hosts))
# Use ThreadPoolExecutor for concurrent scanning
with ThreadPoolExecutor(max_workers=10) as executor:
# Submit all tasks
future_to_ip = {executor.submit(test_plex_server, str(ip)): str(ip)
for ip in network_hosts}
try:
for future in as_completed(future_to_ip):
if self.cancelled:
# Cancel all pending futures
for f in future_to_ip:
if not f.done():
f.cancel()
break
ip = future_to_ip[future]
progress = min(95, progress + progress_step)
self.progress_updated.emit(progress, f"http://{ip}:32400")
try:
result = future.result()
if result:
# Cancel all pending futures before returning
for f in future_to_ip:
if not f.done():
f.cancel()
self.detection_completed.emit(result)
return
except:
pass
finally:
# Ensure executor is properly shutdown
# Use wait=False if cancelled to avoid blocking
executor.shutdown(wait=not self.cancelled)
# If we get here, no Plex server was found
self.progress_updated.emit(100, "Scan complete")
self.detection_completed.emit("") # Empty string = not found
except Exception as e:
print(f"Plex detection error: {e}")
self.detection_completed.emit("") # Empty string = not found
class SlskdDetectionThread(QThread):
progress_updated = pyqtSignal(int, str) # progress value, current url
detection_completed = pyqtSignal(str) # found_url (empty if not found)
@ -180,35 +354,41 @@ class SlskdDetectionThread(QThread):
for target in targets
}
# Process completed tasks
for future in as_completed(future_to_url):
if self.cancelled:
# Cancel remaining futures
for f in future_to_url:
f.cancel()
break
completed_count += 1
progress = int((completed_count / len(targets)) * 100)
current_url = future_to_url[future]
# Update progress
self.progress_updated.emit(progress, f"Scanning {current_url.split('//')[1]}")
# Check result
try:
result_url, confidence = future.result()
if result_url:
found_url = result_url
self.progress_updated.emit(100, f"Found: {result_url}")
# Cancel remaining futures for faster completion
try:
# Process completed tasks
for future in as_completed(future_to_url):
if self.cancelled:
# Cancel remaining futures
for f in future_to_url:
if not f.done():
f.cancel()
break
except:
continue
completed_count += 1
progress = int((completed_count / len(targets)) * 100)
current_url = future_to_url[future]
# Update progress
self.progress_updated.emit(progress, f"Scanning {current_url.split('//')[1]}")
# Check result
try:
result_url, confidence = future.result()
if result_url:
found_url = result_url
self.progress_updated.emit(100, f"Found: {result_url}")
# Cancel remaining futures for faster completion
for f in future_to_url:
if not f.done():
f.cancel()
break
except:
continue
finally:
# Ensure executor is properly shutdown
# Use wait=False if cancelled to avoid blocking
executor.shutdown(wait=not self.cancelled)
return found_url
@ -723,6 +903,110 @@ class SettingsPage(QWidget):
}
self.start_service_test('soulseek', test_config)
def auto_detect_plex(self):
"""Auto-detect Plex server URL using background thread"""
# Don't start new detection if one is already running
if self.detection_thread and self.detection_thread.isRunning():
return
# Create animated loading dialog
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton
from PyQt6.QtCore import QTimer, QPropertyAnimation, QRect
from PyQt6.QtGui import QPainter, QColor
self.detection_dialog = QDialog(self)
self.detection_dialog.setWindowTitle("Auto-detecting Plex Server")
self.detection_dialog.setModal(True)
self.detection_dialog.setFixedSize(400, 180)
self.detection_dialog.setWindowFlags(self.detection_dialog.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
# Apply dark theme styling
self.detection_dialog.setStyleSheet("""
QDialog {
background-color: #282828;
color: #ffffff;
border: 1px solid #404040;
border-radius: 8px;
}
QLabel {
color: #ffffff;
font-size: 14px;
}
QPushButton {
background-color: #404040;
border: 1px solid #606060;
border-radius: 4px;
color: #ffffff;
padding: 8px 16px;
font-size: 11px;
}
QPushButton:hover {
background-color: #505050;
}
""")
layout = QVBoxLayout(self.detection_dialog)
layout.setSpacing(20)
layout.setContentsMargins(20, 20, 20, 20)
# Title label
title_label = QLabel("Searching for Plex servers...")
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title_label)
# Status label
self.status_label = QLabel("Checking local machine...")
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status_label.setStyleSheet("color: #b3b3b3; font-size: 12px;")
layout.addWidget(self.status_label)
# Animated loading bar container
loading_container = QLabel()
loading_container.setFixedHeight(8)
loading_container.setStyleSheet("""
QLabel {
background-color: #404040;
border: 1px solid #606060;
border-radius: 4px;
}
""")
layout.addWidget(loading_container)
# Animated orange bar for Plex
self.loading_bar = QLabel(loading_container)
self.loading_bar.setFixedHeight(6)
self.loading_bar.setStyleSheet("""
background-color: #e5a00d;
border-radius: 3px;
border: none;
""")
# Start animation
self.loading_animation = QPropertyAnimation(self.loading_bar, b"geometry")
self.loading_animation.setDuration(1500) # 1.5 seconds
self.loading_animation.setStartValue(QRect(1, 1, 0, 6))
self.loading_animation.setEndValue(QRect(1, 1, loading_container.width() - 2, 6))
self.loading_animation.setLoopCount(-1) # Infinite loop
self.loading_animation.start()
# Cancel button
button_layout = QHBoxLayout()
button_layout.addStretch()
cancel_btn = QPushButton("Cancel")
cancel_btn.clicked.connect(self.cancel_detection)
button_layout.addWidget(cancel_btn)
layout.addLayout(button_layout)
# Start Plex detection thread
self.detection_thread = PlexDetectionThread()
self.detection_thread.progress_updated.connect(self.on_detection_progress, Qt.ConnectionType.QueuedConnection)
self.detection_thread.detection_completed.connect(self.on_plex_detection_completed, Qt.ConnectionType.QueuedConnection)
self.detection_thread.start()
self.detection_dialog.show()
def auto_detect_slskd(self):
"""Auto-detect slskd URL using background thread"""
# Don't start new detection if one is already running
@ -737,7 +1021,7 @@ class SettingsPage(QWidget):
self.detection_dialog = QDialog(self)
self.detection_dialog.setWindowTitle("Auto-detecting slskd")
self.detection_dialog.setModal(True)
self.detection_dialog.setFixedSize(350, 150)
self.detection_dialog.setFixedSize(400, 180)
self.detection_dialog.setWindowFlags(self.detection_dialog.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
# Apply dark theme styling
@ -830,7 +1114,20 @@ class SettingsPage(QWidget):
def cancel_detection(self):
"""Cancel the ongoing detection"""
if self.detection_thread:
# Set cancellation flag first
self.detection_thread.cancel()
# If thread is still running, terminate it
if self.detection_thread.isRunning():
self.detection_thread.quit()
# Don't wait too long during cancellation to avoid blocking UI
if not self.detection_thread.wait(500): # Wait only 500ms
# Force terminate if it doesn't respond
self.detection_thread.terminate()
self.detection_thread.wait()
self.detection_thread.deleteLater()
self.detection_thread = None
# Close dialog
if hasattr(self, 'detection_dialog') and self.detection_dialog:
@ -847,8 +1144,40 @@ class SettingsPage(QWidget):
else:
self.status_label.setText("Checking network...")
def on_plex_detection_completed(self, found_url):
"""Handle Plex detection completion"""
# Stop animation and close dialog
if hasattr(self, 'loading_animation'):
self.loading_animation.stop()
if hasattr(self, 'detection_dialog') and self.detection_dialog:
self.detection_dialog.close()
self.detection_dialog = None
# Properly cleanup thread
if self.detection_thread:
if self.detection_thread.isRunning():
self.detection_thread.quit()
self.detection_thread.wait(1000) # Wait up to 1 second for thread to finish
self.detection_thread.deleteLater()
self.detection_thread = None
if found_url:
self.plex_url_input.setText(found_url)
self.show_plex_success_dialog(found_url)
else:
QMessageBox.warning(self, "Auto-detect Failed",
"Could not find Plex server running on local machine or network.\n\n"
"Please ensure Plex Media Server is running and try:\n"
"• Check if Plex Media Server service is started\n"
"• Verify firewall allows access to Plex port (32400)\n"
"• Enter the URL manually if on a different network\n\n"
"Common URLs:\n"
"• http://localhost:32400 (local default)\n"
"• http://192.168.1.100:32400 (network example)")
def on_detection_completed(self, found_url):
"""Handle detection completion"""
"""Handle slskd detection completion"""
# Stop animation and close dialog
if hasattr(self, 'loading_animation'):
self.loading_animation.stop()
@ -857,7 +1186,11 @@ class SettingsPage(QWidget):
self.detection_dialog.close()
self.detection_dialog = None
# Properly cleanup thread
if self.detection_thread:
if self.detection_thread.isRunning():
self.detection_thread.quit()
self.detection_thread.wait(1000) # Wait up to 1 second for thread to finish
self.detection_thread.deleteLater()
self.detection_thread = None
@ -875,8 +1208,119 @@ class SettingsPage(QWidget):
"• http://localhost:5030 (local default)\n"
"• http://192.168.1.100:5030 (network example)")
def show_plex_success_dialog(self, found_url):
"""Show custom Plex success dialog with copy functionality"""
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTextEdit
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QClipboard
dialog = QDialog(self)
dialog.setWindowTitle("Plex Auto-detect Success")
dialog.setModal(True)
dialog.setFixedSize(380, 160)
dialog.setWindowFlags(dialog.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)
# Apply dark theme styling
dialog.setStyleSheet("""
QDialog {
background-color: #282828;
color: #ffffff;
border: 1px solid #404040;
border-radius: 8px;
}
QLabel {
color: #ffffff;
font-size: 12px;
}
QTextEdit {
background-color: #404040;
border: 1px solid #606060;
border-radius: 4px;
color: #ffffff;
font-size: 11px;
font-family: 'Courier New', monospace;
padding: 8px;
}
QPushButton {
background-color: #404040;
border: 1px solid #606060;
border-radius: 4px;
color: #ffffff;
padding: 6px 12px;
font-size: 11px;
min-width: 50px;
min-height: 28px;
}
QPushButton:hover {
background-color: #505050;
}
#copyButton {
background-color: #e5a00d;
border: 1px solid #e5a00d;
color: #000000;
font-weight: bold;
min-height: 28px;
}
#copyButton:hover {
background-color: #f5b00d;
}
""")
layout = QVBoxLayout(dialog)
layout.setSpacing(8)
layout.setContentsMargins(15, 15, 15, 15)
# Success message
location_type = "locally" if "localhost" in found_url or "127.0.0.1" in found_url else "on network"
success_label = QLabel(f"✓ Found Plex server running {location_type}!")
success_label.setStyleSheet("color: #e5a00d; font-size: 13px; font-weight: bold;")
success_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(success_label)
# URL display with copy functionality
url_label = QLabel("Detected URL:")
layout.addWidget(url_label)
url_container = QHBoxLayout()
url_container.setSpacing(5)
url_display = QTextEdit()
url_display.setPlainText(found_url)
url_display.setReadOnly(True)
url_display.setFixedHeight(30)
url_display.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
url_container.addWidget(url_display)
copy_btn = QPushButton("Copy")
copy_btn.setObjectName("copyButton")
copy_btn.setFixedSize(55, 30)
copy_btn.clicked.connect(lambda: self.copy_to_clipboard(found_url, copy_btn))
url_container.addWidget(copy_btn)
layout.addLayout(url_container)
# Info text
info_label = QLabel("URL automatically filled in settings above.")
info_label.setStyleSheet("color: #b3b3b3; font-size: 9px; font-style: italic;")
info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(info_label)
# OK button
button_layout = QHBoxLayout()
button_layout.addStretch()
ok_btn = QPushButton("OK")
ok_btn.setFixedSize(60, 28)
ok_btn.clicked.connect(dialog.accept)
ok_btn.setDefault(True)
button_layout.addWidget(ok_btn)
layout.addLayout(button_layout)
dialog.exec()
def show_success_dialog(self, found_url):
"""Show custom success dialog with copy functionality"""
"""Show custom slskd success dialog with copy functionality"""
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTextEdit
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QClipboard
@ -1146,11 +1590,20 @@ class SettingsPage(QWidget):
plex_url_label.setStyleSheet("color: #ffffff; font-size: 11px;")
plex_layout.addWidget(plex_url_label)
plex_url_input_layout = QHBoxLayout()
self.plex_url_input = QLineEdit()
self.plex_url_input.setStyleSheet(self.get_input_style())
self.plex_url_input.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
self.form_inputs['plex.base_url'] = self.plex_url_input
plex_layout.addWidget(self.plex_url_input)
plex_detect_btn = QPushButton("Auto-detect")
plex_detect_btn.setFixedSize(80, 30)
plex_detect_btn.clicked.connect(self.auto_detect_plex)
plex_detect_btn.setStyleSheet(self.get_test_button_style())
plex_url_input_layout.addWidget(self.plex_url_input)
plex_url_input_layout.addWidget(plex_detect_btn)
plex_layout.addLayout(plex_url_input_layout)
# Token
plex_token_label = QLabel("Token:")

Loading…
Cancel
Save