settings auto detect fixed

This commit is contained in:
Broque Thomas 2025-08-22 14:36:30 -07:00
parent d631ad61f8
commit 36b3830a2f
4 changed files with 556 additions and 37 deletions

View file

@ -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 ---

View file

@ -463,6 +463,29 @@
<!-- Toast Notifications -->
<div class="toast-container" id="toast-container"></div>
<!-- Version Info Modal -->
<div class="version-modal-overlay hidden" id="version-modal-overlay" onclick="closeVersionModal()">
<div class="version-modal" onclick="event.stopPropagation()">
<!-- Header -->
<div class="version-modal-header">
<h2 class="version-modal-title">What's New in SoulSync</h2>
<div class="version-modal-subtitle">Version 0.65 - Tidal Playlist Integration</div>
</div>
<!-- Content Area with Scroll -->
<div class="version-modal-content">
<div class="version-content-container" id="version-content-container">
<!-- Content will be populated by JavaScript -->
</div>
</div>
<!-- Footer -->
<div class="version-modal-footer">
<button class="version-modal-close" onclick="closeVersionModal()">Close</button>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>

View file

@ -993,8 +993,99 @@ function escapeHtml(text) {
return div.innerHTML;
}
function showVersionInfo() {
showToast('Version info modal not implemented yet', 'error');
async function showVersionInfo() {
try {
console.log('Fetching version info...');
// Fetch version data from API
const response = await fetch('/api/version-info');
if (!response.ok) {
throw new Error('Failed to fetch version info');
}
const versionData = await response.json();
console.log('Version data received:', versionData);
// Populate modal content
populateVersionModal(versionData);
// Show modal
const modalOverlay = document.getElementById('version-modal-overlay');
modalOverlay.classList.remove('hidden');
console.log('Version modal opened');
} catch (error) {
console.error('Error showing version info:', error);
showToast('Failed to load version information', 'error');
}
}
function closeVersionModal() {
const modalOverlay = document.getElementById('version-modal-overlay');
modalOverlay.classList.add('hidden');
console.log('Version modal closed');
}
function populateVersionModal(versionData) {
const container = document.getElementById('version-content-container');
if (!container) {
console.error('Version content container not found');
return;
}
// Update header with dynamic data
const titleElement = document.querySelector('.version-modal-title');
const subtitleElement = document.querySelector('.version-modal-subtitle');
if (titleElement) titleElement.textContent = versionData.title;
if (subtitleElement) subtitleElement.textContent = versionData.subtitle;
// Clear existing content
container.innerHTML = '';
// Create sections
versionData.sections.forEach(section => {
const sectionDiv = document.createElement('div');
sectionDiv.className = 'version-feature-section';
// Section title
const titleDiv = document.createElement('div');
titleDiv.className = 'version-section-title';
titleDiv.textContent = section.title;
sectionDiv.appendChild(titleDiv);
// Section description
const descDiv = document.createElement('div');
descDiv.className = 'version-section-description';
descDiv.textContent = section.description;
sectionDiv.appendChild(descDiv);
// Features list
const featuresList = document.createElement('ul');
featuresList.className = 'version-feature-list';
section.features.forEach(feature => {
const featureItem = document.createElement('li');
featureItem.className = 'version-feature-item';
featureItem.textContent = feature;
featuresList.appendChild(featureItem);
});
sectionDiv.appendChild(featuresList);
// Usage note (if present)
if (section.usage_note) {
const usageDiv = document.createElement('div');
usageDiv.className = 'version-usage-note';
usageDiv.textContent = `💡 ${section.usage_note}`;
sectionDiv.appendChild(usageDiv);
}
container.appendChild(sectionDiv);
});
console.log('Version modal content populated');
}
// ===============================
@ -1174,6 +1265,7 @@ window.navigateToPage = navigateToPage;
window.openKofi = openKofi;
window.copyAddress = copyAddress;
window.showVersionInfo = showVersionInfo;
window.closeVersionModal = closeVersionModal;
window.testConnection = testConnection;
window.autoDetectPlex = autoDetectPlex;
window.autoDetectJellyfin = autoDetectJellyfin;

View file

@ -392,7 +392,7 @@ body {
.crypto-donation {
background: linear-gradient(180deg, transparent 0%, rgba(255, 255, 255, 0.02) 30%, rgba(255, 255, 255, 0.04) 100%);
border-top: 1px solid rgba(255, 255, 255, 0.08);
border-bottom-right-radius: 12px;
padding: 15px 0;
}
@ -474,8 +474,8 @@ body {
height: 45px;
background: linear-gradient(180deg, transparent 0%, rgba(255, 255, 255, 0.02) 30%, rgba(255, 255, 255, 0.04) 100%);
border-top: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
margin: 0 10px;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
@ -494,6 +494,7 @@ body {
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.version-button:hover {
@ -508,7 +509,7 @@ body {
/* Status Section - Matching status indicators */
.status-section {
height: 150px;
height: fit-content;
background: linear-gradient(180deg, transparent 0%, rgba(255, 255, 255, 0.02) 30%, rgba(255, 255, 255, 0.04) 100%);
border-top: 1px solid rgba(255, 255, 255, 0.08);
border-bottom-right-radius: 12px;
@ -1428,4 +1429,203 @@ body {
.settings-content {
max-width: 900px; /* Wider settings forms */
}
}
/* ===== VERSION MODAL STYLES ===== */
.version-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(8px);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
opacity: 1;
transition: opacity 0.3s ease-in-out;
}
.version-modal-overlay.hidden {
opacity: 0;
pointer-events: none;
}
.version-modal {
background: #1a1a1a;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
width: 600px;
max-width: 90vw;
height: 500px;
max-height: 90vh;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
transform: scale(1);
transition: transform 0.3s ease-in-out;
}
.version-modal-overlay.hidden .version-modal {
transform: scale(0.9);
}
/* Header */
.version-modal-header {
padding: 30px 30px 15px 30px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
border-top-left-radius: 12px;
border-top-right-radius: 12px;
background: #1a1a1a;
}
.version-modal-title {
color: #ffffff;
font-size: 18px;
font-weight: 700;
letter-spacing: -0.5px;
margin: 0 0 5px 0;
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
.version-modal-subtitle {
color: rgba(255, 255, 255, 0.7);
font-size: 11px;
font-weight: 500;
letter-spacing: 0.1px;
margin: 0;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
/* Content Area */
.version-modal-content {
flex: 1;
padding: 0;
overflow-y: auto;
background: #1a1a1a;
}
.version-modal-content::-webkit-scrollbar {
width: 8px;
}
.version-modal-content::-webkit-scrollbar-track {
background: #2a2a2a;
border-radius: 4px;
}
.version-modal-content::-webkit-scrollbar-thumb {
background: #555555;
border-radius: 4px;
}
.version-modal-content::-webkit-scrollbar-thumb:hover {
background: #666666;
}
.version-content-container {
padding: 25px 30px;
}
/* Feature Sections */
.version-feature-section {
margin-bottom: 25px;
padding: 18px 20px;
border-left: 3px solid rgba(29, 185, 84, 0.4);
margin-left: 5px;
background: transparent;
}
.version-section-title {
color: #1ed760;
font-size: 14px;
font-weight: 600;
letter-spacing: -0.2px;
margin: 0 0 8px 0;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
.version-section-description {
color: rgba(255, 255, 255, 0.8);
font-size: 11px;
line-height: 1.4;
margin: 0 0 12px 0;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
.version-feature-list {
list-style: none;
padding: 0;
margin: 0;
}
.version-feature-item {
color: rgba(255, 255, 255, 0.7);
font-size: 10px;
line-height: 1.5;
padding: 2px 0 2px 8px;
margin: 2px 0;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
.version-usage-note {
color: #1ed760;
font-size: 10px;
line-height: 1.4;
margin: 8px 0 0 0;
padding: 8px 0;
font-style: italic;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
/* Footer */
.version-modal-footer {
padding: 15px 30px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
background: rgba(255, 255, 255, 0.02);
display: flex;
justify-content: flex-end;
}
.version-modal-close {
background: #1db954;
color: white;
border: none;
border-radius: 6px;
padding: 8px 25px;
font-size: 10px;
font-weight: 500;
letter-spacing: 0.1px;
cursor: pointer;
transition: background-color 0.2s ease;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
.version-modal-close:hover {
background: #1ed760;
}
.version-modal-close:active {
background: #169c46;
}
/* Animation for modal appearance */
@keyframes modalFadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.version-modal-overlay:not(.hidden) .version-modal {
animation: modalFadeIn 0.3s ease-out;
}