|
|
|
|
@ -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:")
|
|
|
|
|
|