gui navidrome support
This commit is contained in:
parent
41df3d84b7
commit
ba37225e1b
7 changed files with 489 additions and 45 deletions
|
|
@ -20,11 +20,17 @@
|
|||
"api_key": "JELLYFIN_API_KEY",
|
||||
"auto_detect": true
|
||||
},
|
||||
"navidrome": {
|
||||
"base_url": "http://localhost:4533",
|
||||
"username": "NAVIDROME_USERNAME",
|
||||
"password": "NAVIDROME_PASSWORD",
|
||||
"auto_detect": true
|
||||
},
|
||||
"soulseek": {
|
||||
"slskd_url": "http://localhost:5030",
|
||||
"slskd_url": "http://host.docker.internal:5030",
|
||||
"api_key": "SoulseekAPIKey",
|
||||
"download_path": "./Downloads",
|
||||
"transfer_path": "./Transfer"
|
||||
"download_path": "/app/downloads",
|
||||
"transfer_path": "/app/Transfer"
|
||||
},
|
||||
"logging": {
|
||||
"path": "logs/app.log",
|
||||
|
|
|
|||
BIN
ui/assets/navidrome_icon.png
Normal file
BIN
ui/assets/navidrome_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -1943,6 +1943,7 @@ class DashboardDataProvider(QObject):
|
|||
'spotify': ServiceStatus('Spotify', False, datetime.now()),
|
||||
'plex': ServiceStatus('Plex', False, datetime.now()),
|
||||
'jellyfin': ServiceStatus('Jellyfin', False, datetime.now()),
|
||||
'navidrome': ServiceStatus('Navidrome', False, datetime.now()),
|
||||
'soulseek': ServiceStatus('Soulseek', False, datetime.now())
|
||||
}
|
||||
self.download_stats = DownloadStats()
|
||||
|
|
@ -2100,6 +2101,7 @@ class DashboardDataProvider(QObject):
|
|||
'spotify': 'spotify_client',
|
||||
'plex': 'plex_client',
|
||||
'jellyfin': None, # Jellyfin doesn't need a client, tests via config
|
||||
'navidrome': 'navidrome_client',
|
||||
'soulseek': 'soulseek_client'
|
||||
}
|
||||
|
||||
|
|
@ -3053,7 +3055,12 @@ class DashboardPage(QWidget):
|
|||
# Create service status cards with dynamic media server
|
||||
from config.settings import config_manager
|
||||
active_server = config_manager.get_active_media_server()
|
||||
server_name = "Plex" if active_server == "plex" else "Jellyfin"
|
||||
server_name_map = {
|
||||
'plex': 'Plex',
|
||||
'jellyfin': 'Jellyfin',
|
||||
'navidrome': 'Navidrome'
|
||||
}
|
||||
server_name = server_name_map.get(active_server, 'Jellyfin')
|
||||
services = ['Spotify', server_name, 'Soulseek']
|
||||
for service in services:
|
||||
card = ServiceStatusCard(service)
|
||||
|
|
|
|||
|
|
@ -904,6 +904,125 @@ class JellyfinDetectionThread(QThread):
|
|||
|
||||
return False
|
||||
|
||||
class NavidromeDetectionThread(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
|
||||
|
||||
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()
|
||||
|
||||
# Parse network info
|
||||
network = ipaddress.ip_network(f"{local_ip}/24", strict=False)
|
||||
return {
|
||||
'local_ip': local_ip,
|
||||
'network': network,
|
||||
'subnet': str(network.network_address) + "/24"
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error getting network info: {e}")
|
||||
return {'local_ip': '127.0.0.1', 'network': None, 'subnet': '127.0.0.1/32'}
|
||||
|
||||
try:
|
||||
# Test common Navidrome URLs first
|
||||
common_urls = [
|
||||
"http://localhost:4533",
|
||||
"http://127.0.0.1:4533",
|
||||
"http://navidrome:4533"
|
||||
]
|
||||
|
||||
network_info = get_network_info()
|
||||
local_ip = network_info['local_ip']
|
||||
|
||||
# Add local IP with common ports
|
||||
common_urls.extend([
|
||||
f"http://{local_ip}:4533"
|
||||
])
|
||||
|
||||
total_hosts = len(common_urls)
|
||||
current_host = 0
|
||||
|
||||
# Test common URLs first
|
||||
for url in common_urls:
|
||||
if self.cancelled:
|
||||
break
|
||||
|
||||
current_host += 1
|
||||
self.progress_updated.emit(int((current_host / total_hosts) * 100), url)
|
||||
|
||||
if self.test_navidrome_url(url):
|
||||
self.detection_completed.emit(url)
|
||||
return
|
||||
|
||||
# If no common URLs worked, signal not found
|
||||
self.detection_completed.emit("")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Navidrome detection error: {e}")
|
||||
self.detection_completed.emit("")
|
||||
|
||||
def test_navidrome_url(self, url, timeout=5):
|
||||
"""Test if URL hosts a Navidrome server by checking for ping endpoint"""
|
||||
try:
|
||||
# Test Navidrome ping endpoint
|
||||
ping_url = f"{url.rstrip('/')}/rest/ping"
|
||||
print(f"Testing Navidrome at: {ping_url}")
|
||||
|
||||
response = requests.get(ping_url, params={
|
||||
'u': 'test',
|
||||
't': 'test',
|
||||
's': 'test',
|
||||
'v': '1.16.1',
|
||||
'c': 'SoulSync',
|
||||
'f': 'json'
|
||||
}, timeout=timeout)
|
||||
|
||||
print(f"Response status: {response.status_code}")
|
||||
|
||||
# Navidrome should return status 401 or 403 for invalid credentials, not 404
|
||||
if response.status_code in [200, 401, 403]:
|
||||
try:
|
||||
data = response.json()
|
||||
print(f"Response data: {data}")
|
||||
# Check if it's a valid Subsonic API response
|
||||
if 'subsonic-response' in data:
|
||||
print(f"✓ Found Navidrome server at {url}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"JSON parse error: {e}")
|
||||
|
||||
# Also try a simple GET to the root to see if it's at least a web server
|
||||
try:
|
||||
root_response = requests.get(url, timeout=timeout)
|
||||
if root_response.status_code == 200 and 'navidrome' in root_response.text.lower():
|
||||
print(f"✓ Found Navidrome web interface at {url}")
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error testing {url}: {e}")
|
||||
return False
|
||||
|
||||
class SettingsGroup(QGroupBox):
|
||||
def __init__(self, title: str, parent=None):
|
||||
super().__init__(title, parent)
|
||||
|
|
@ -1091,19 +1210,29 @@ class SettingsPage(QWidget):
|
|||
jellyfin_config = config_manager.get_jellyfin_config()
|
||||
self.jellyfin_url_input.setText(jellyfin_config.get('base_url', ''))
|
||||
self.jellyfin_api_key_input.setText(jellyfin_config.get('api_key', ''))
|
||||
|
||||
|
||||
# Load Navidrome config
|
||||
navidrome_config = config_manager.get_navidrome_config()
|
||||
self.navidrome_url_input.setText(navidrome_config.get('base_url', ''))
|
||||
self.navidrome_username_input.setText(navidrome_config.get('username', ''))
|
||||
self.navidrome_password_input.setText(navidrome_config.get('password', ''))
|
||||
|
||||
# Initialize server selection
|
||||
active_server = config_manager.get_active_media_server()
|
||||
self.pending_server_change = None
|
||||
self.update_server_toggle_styles(active_server)
|
||||
|
||||
# Show/hide appropriate containers based on active server
|
||||
self.plex_container.hide()
|
||||
self.jellyfin_container.hide()
|
||||
self.navidrome_container.hide()
|
||||
|
||||
if active_server == 'plex':
|
||||
self.plex_container.show()
|
||||
self.jellyfin_container.hide()
|
||||
else:
|
||||
self.plex_container.hide()
|
||||
elif active_server == 'jellyfin':
|
||||
self.jellyfin_container.show()
|
||||
elif active_server == 'navidrome':
|
||||
self.navidrome_container.show()
|
||||
|
||||
# Load Soulseek config
|
||||
soulseek_config = config_manager.get_soulseek_config()
|
||||
|
|
@ -1178,7 +1307,12 @@ class SettingsPage(QWidget):
|
|||
# Save Jellyfin settings
|
||||
config_manager.set('jellyfin.base_url', self.jellyfin_url_input.text())
|
||||
config_manager.set('jellyfin.api_key', self.jellyfin_api_key_input.text())
|
||||
|
||||
|
||||
# Save Navidrome settings
|
||||
config_manager.set('navidrome.base_url', self.navidrome_url_input.text())
|
||||
config_manager.set('navidrome.username', self.navidrome_username_input.text())
|
||||
config_manager.set('navidrome.password', self.navidrome_password_input.text())
|
||||
|
||||
# Save pending server change if any
|
||||
if hasattr(self, 'pending_server_change') and self.pending_server_change:
|
||||
config_manager.set_active_media_server(self.pending_server_change)
|
||||
|
|
@ -2260,9 +2394,16 @@ class SettingsPage(QWidget):
|
|||
self.jellyfin_toggle_button.setFixedHeight(40)
|
||||
self.jellyfin_toggle_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
self.jellyfin_toggle_button.clicked.connect(lambda: self.select_media_server('jellyfin'))
|
||||
|
||||
|
||||
# Navidrome toggle button
|
||||
self.navidrome_toggle_button = QPushButton()
|
||||
self.navidrome_toggle_button.setFixedHeight(40)
|
||||
self.navidrome_toggle_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
self.navidrome_toggle_button.clicked.connect(lambda: self.select_media_server('navidrome'))
|
||||
|
||||
toggle_container.addWidget(self.plex_toggle_button)
|
||||
toggle_container.addWidget(self.jellyfin_toggle_button)
|
||||
toggle_container.addWidget(self.navidrome_toggle_button)
|
||||
server_selection_layout.addLayout(toggle_container)
|
||||
|
||||
# Restart warning (initially hidden)
|
||||
|
|
@ -2397,7 +2538,78 @@ class SettingsPage(QWidget):
|
|||
|
||||
# Add Jellyfin frame to its container
|
||||
jellyfin_container_layout.addWidget(jellyfin_frame)
|
||||
|
||||
|
||||
# Navidrome Settings Container
|
||||
self.navidrome_container = QWidget()
|
||||
self.navidrome_container.setStyleSheet("background: transparent;")
|
||||
navidrome_container_layout = QVBoxLayout(self.navidrome_container)
|
||||
navidrome_container_layout.setContentsMargins(0, 0, 0, 0)
|
||||
navidrome_container_layout.setSpacing(0)
|
||||
|
||||
# Navidrome settings
|
||||
navidrome_frame = QFrame()
|
||||
navidrome_frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background: #333333;
|
||||
border: 1px solid #444444;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
""")
|
||||
navidrome_layout = QVBoxLayout(navidrome_frame)
|
||||
navidrome_layout.setSpacing(8)
|
||||
|
||||
navidrome_title = QLabel("Navidrome")
|
||||
navidrome_title.setFont(QFont("Arial", 12, QFont.Weight.Bold))
|
||||
navidrome_title.setStyleSheet("color: #ff6b6b;") # Navidrome red color
|
||||
navidrome_layout.addWidget(navidrome_title)
|
||||
|
||||
# Server URL
|
||||
navidrome_url_label = QLabel("Server URL:")
|
||||
navidrome_url_label.setStyleSheet(self.get_label_style(11))
|
||||
navidrome_layout.addWidget(navidrome_url_label)
|
||||
|
||||
navidrome_url_input_layout = QHBoxLayout()
|
||||
self.navidrome_url_input = QLineEdit()
|
||||
self.navidrome_url_input.setStyleSheet(self.get_input_style())
|
||||
self.navidrome_url_input.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
self.form_inputs['navidrome.base_url'] = self.navidrome_url_input
|
||||
|
||||
navidrome_detect_btn = QPushButton("Auto-detect")
|
||||
navidrome_detect_btn.setFixedSize(80, 30)
|
||||
navidrome_detect_btn.clicked.connect(self.auto_detect_navidrome)
|
||||
navidrome_detect_btn.setStyleSheet(self.get_test_button_style())
|
||||
|
||||
navidrome_url_input_layout.addWidget(self.navidrome_url_input)
|
||||
navidrome_url_input_layout.addWidget(navidrome_detect_btn)
|
||||
navidrome_layout.addLayout(navidrome_url_input_layout)
|
||||
|
||||
# Username
|
||||
navidrome_username_label = QLabel("Username:")
|
||||
navidrome_username_label.setStyleSheet(self.get_label_style(11))
|
||||
navidrome_layout.addWidget(navidrome_username_label)
|
||||
|
||||
self.navidrome_username_input = QLineEdit()
|
||||
self.navidrome_username_input.setStyleSheet(self.get_input_style())
|
||||
self.navidrome_username_input.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
self.form_inputs['navidrome.username'] = self.navidrome_username_input
|
||||
navidrome_layout.addWidget(self.navidrome_username_input)
|
||||
|
||||
# Password
|
||||
navidrome_password_label = QLabel("Password:")
|
||||
navidrome_password_label.setStyleSheet(self.get_label_style(11))
|
||||
navidrome_layout.addWidget(navidrome_password_label)
|
||||
|
||||
self.navidrome_password_input = QLineEdit()
|
||||
self.navidrome_password_input.setEchoMode(QLineEdit.EchoMode.Password)
|
||||
self.navidrome_password_input.setStyleSheet(self.get_input_style())
|
||||
self.navidrome_password_input.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
self.form_inputs['navidrome.password'] = self.navidrome_password_input
|
||||
navidrome_layout.addWidget(self.navidrome_password_input)
|
||||
|
||||
# Add Navidrome frame to its container
|
||||
navidrome_container_layout.addWidget(navidrome_frame)
|
||||
|
||||
# Soulseek settings
|
||||
soulseek_frame = QFrame()
|
||||
soulseek_frame.setStyleSheet("""
|
||||
|
|
@ -2454,6 +2666,7 @@ class SettingsPage(QWidget):
|
|||
api_layout.addWidget(server_selection_container)
|
||||
api_layout.addWidget(self.plex_container)
|
||||
api_layout.addWidget(self.jellyfin_container)
|
||||
api_layout.addWidget(self.navidrome_container)
|
||||
api_layout.addWidget(soulseek_frame)
|
||||
|
||||
# Test connections
|
||||
|
|
@ -2795,12 +3008,16 @@ class SettingsPage(QWidget):
|
|||
self.update_server_toggle_styles(server_type)
|
||||
|
||||
# Show/hide appropriate containers
|
||||
self.plex_container.hide()
|
||||
self.jellyfin_container.hide()
|
||||
self.navidrome_container.hide()
|
||||
|
||||
if server_type == 'plex':
|
||||
self.plex_container.show()
|
||||
self.jellyfin_container.hide()
|
||||
else:
|
||||
self.plex_container.hide()
|
||||
elif server_type == 'jellyfin':
|
||||
self.jellyfin_container.show()
|
||||
elif server_type == 'navidrome':
|
||||
self.navidrome_container.show()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error selecting media server: {e}")
|
||||
|
|
@ -2870,9 +3087,21 @@ class SettingsPage(QWidget):
|
|||
"jellyfin_icon.png",
|
||||
32
|
||||
)
|
||||
|
||||
|
||||
if not hasattr(self, '_cached_navidrome_icon'):
|
||||
self._cached_navidrome_icon = download_and_cache_logo(
|
||||
"https://raw.githubusercontent.com/navidrome/navidrome/master/resources/logo-192x192.png",
|
||||
"navidrome_icon.png",
|
||||
32
|
||||
)
|
||||
# Fallback to a simple text-based icon if download fails
|
||||
if self._cached_navidrome_icon.isNull():
|
||||
logger.warning("Navidrome icon download failed, creating fallback icon")
|
||||
self._cached_navidrome_icon = self._create_fallback_icon("N", "#ff6b6b")
|
||||
|
||||
plex_icon = self._cached_plex_icon
|
||||
jellyfin_icon = self._cached_jellyfin_icon
|
||||
navidrome_icon = self._cached_navidrome_icon
|
||||
|
||||
# Active button styles with appropriate colors
|
||||
active_plex_style = """
|
||||
|
|
@ -2904,6 +3133,21 @@ class SettingsPage(QWidget):
|
|||
stop:1 rgba(150, 82, 175, 1.0));
|
||||
}
|
||||
"""
|
||||
|
||||
active_navidrome_style = """
|
||||
QPushButton {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop:0 rgba(255, 107, 107, 0.8),
|
||||
stop:1 rgba(235, 87, 87, 0.9));
|
||||
border: 2px solid rgba(255, 107, 107, 1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
||||
stop:0 rgba(255, 107, 107, 0.9),
|
||||
stop:1 rgba(235, 87, 87, 1.0));
|
||||
}
|
||||
"""
|
||||
|
||||
# Inactive button style
|
||||
inactive_style = """
|
||||
|
|
@ -2918,19 +3162,64 @@ class SettingsPage(QWidget):
|
|||
}
|
||||
"""
|
||||
|
||||
# Set icons and styles
|
||||
# Set icons, text, and styles
|
||||
self.plex_toggle_button.setIcon(plex_icon)
|
||||
self.plex_toggle_button.setIconSize(QSize(28, 28))
|
||||
self.plex_toggle_button.setText("Plex")
|
||||
|
||||
self.jellyfin_toggle_button.setIcon(jellyfin_icon)
|
||||
self.jellyfin_toggle_button.setIconSize(QSize(28, 28))
|
||||
|
||||
self.jellyfin_toggle_button.setText("Jellyfin")
|
||||
|
||||
self.navidrome_toggle_button.setIcon(navidrome_icon)
|
||||
self.navidrome_toggle_button.setIconSize(QSize(28, 28))
|
||||
self.navidrome_toggle_button.setText("Navidrome")
|
||||
|
||||
# Debug: Check if icons are properly loaded
|
||||
if navidrome_icon.isNull():
|
||||
logger.warning("Navidrome icon failed to load!")
|
||||
else:
|
||||
logger.info("Navidrome icon loaded successfully")
|
||||
|
||||
# Reset all buttons to inactive first
|
||||
self.plex_toggle_button.setStyleSheet(inactive_style)
|
||||
self.jellyfin_toggle_button.setStyleSheet(inactive_style)
|
||||
self.navidrome_toggle_button.setStyleSheet(inactive_style)
|
||||
|
||||
# Set the active server button style
|
||||
if active_server == 'plex':
|
||||
self.plex_toggle_button.setStyleSheet(active_plex_style)
|
||||
self.jellyfin_toggle_button.setStyleSheet(inactive_style)
|
||||
else:
|
||||
self.plex_toggle_button.setStyleSheet(inactive_style)
|
||||
elif active_server == 'jellyfin':
|
||||
self.jellyfin_toggle_button.setStyleSheet(active_jellyfin_style)
|
||||
|
||||
elif active_server == 'navidrome':
|
||||
self.navidrome_toggle_button.setStyleSheet(active_navidrome_style)
|
||||
|
||||
def _create_fallback_icon(self, text, color):
|
||||
"""Create a simple text-based fallback icon"""
|
||||
from PyQt6.QtGui import QPixmap, QPainter, QFont, QColor
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
# Create a 32x32 pixmap
|
||||
pixmap = QPixmap(32, 32)
|
||||
pixmap.fill(Qt.GlobalColor.transparent)
|
||||
|
||||
painter = QPainter(pixmap)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
# Draw background circle
|
||||
painter.setBrush(QColor(color))
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.drawEllipse(2, 2, 28, 28)
|
||||
|
||||
# Draw text
|
||||
painter.setPen(QColor("white"))
|
||||
font = QFont("Arial", 14, QFont.Weight.Bold)
|
||||
painter.setFont(font)
|
||||
painter.drawText(0, 0, 32, 32, Qt.AlignmentFlag.AlignCenter, text)
|
||||
|
||||
painter.end()
|
||||
return QIcon(pixmap)
|
||||
|
||||
def auto_detect_jellyfin(self):
|
||||
"""Auto-detect Jellyfin server URL using background thread"""
|
||||
# Don't start new detection if one is already running
|
||||
|
|
@ -3034,7 +3323,102 @@ class SettingsPage(QWidget):
|
|||
self.detection_thread.start()
|
||||
|
||||
self.detection_dialog.show()
|
||||
|
||||
|
||||
def auto_detect_navidrome(self):
|
||||
"""Auto-detect Navidrome 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 Navidrome 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)
|
||||
|
||||
# Status text
|
||||
status_label = QLabel("Scanning network for Navidrome servers...")
|
||||
status_label.setWordWrap(True)
|
||||
layout.addWidget(status_label)
|
||||
|
||||
# Cancel button
|
||||
button_layout = QHBoxLayout()
|
||||
cancel_btn = QPushButton("Cancel")
|
||||
cancel_btn.clicked.connect(self.cancel_detection)
|
||||
button_layout.addStretch()
|
||||
button_layout.addWidget(cancel_btn)
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# Start Navidrome detection thread
|
||||
self.detection_thread = NavidromeDetectionThread()
|
||||
self.detection_thread.progress_updated.connect(self.on_detection_progress, Qt.ConnectionType.QueuedConnection)
|
||||
self.detection_thread.detection_completed.connect(self.on_navidrome_detection_completed, Qt.ConnectionType.QueuedConnection)
|
||||
self.detection_thread.start()
|
||||
|
||||
self.detection_dialog.show()
|
||||
|
||||
def on_navidrome_detection_completed(self, found_url):
|
||||
"""Handle Navidrome 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
|
||||
self.detection_thread = None
|
||||
|
||||
if found_url:
|
||||
self.navidrome_url_input.setText(found_url)
|
||||
# Show success toast
|
||||
from ui.components.toast_manager import ToastManager
|
||||
toast_manager = ToastManager(self)
|
||||
toast_manager.show_toast(f"✓ Navidrome server detected: {found_url}", "success", 4000)
|
||||
else:
|
||||
# Show error toast
|
||||
from ui.components.toast_manager import ToastManager
|
||||
toast_manager = ToastManager(self)
|
||||
toast_manager.show_toast("❌ No Navidrome servers found on the network", "error", 4000)
|
||||
|
||||
def on_jellyfin_detection_completed(self, found_url):
|
||||
"""Handle Jellyfin detection completion"""
|
||||
# Stop animation and close dialog
|
||||
|
|
|
|||
|
|
@ -1709,7 +1709,8 @@ class PlaylistDetailsModal(QDialog):
|
|||
self.parent_page.spotify_client,
|
||||
self.parent_page.plex_client,
|
||||
self.parent_page.soulseek_client,
|
||||
getattr(self.parent_page, 'jellyfin_client', None)
|
||||
getattr(self.parent_page, 'jellyfin_client', None),
|
||||
getattr(self.parent_page, 'navidrome_client', None)
|
||||
)
|
||||
|
||||
# Start sync
|
||||
|
|
@ -1839,7 +1840,15 @@ class PlaylistDetailsModal(QDialog):
|
|||
|
||||
def on_analysis_started(self, total_tracks):
|
||||
"""Handle analysis started signal"""
|
||||
print(f"Started analyzing {total_tracks} tracks against Plex library")
|
||||
# Get server name for log message
|
||||
try:
|
||||
from config.settings import config_manager
|
||||
active_server = config_manager.get_active_media_server()
|
||||
server_name = active_server.title() if active_server else "Plex"
|
||||
except:
|
||||
server_name = "Plex"
|
||||
|
||||
print(f"Started analyzing {total_tracks} tracks against {server_name} library")
|
||||
|
||||
def on_track_analyzed(self, track_index, result):
|
||||
"""Handle individual track analysis completion"""
|
||||
|
|
@ -1858,12 +1867,20 @@ class PlaylistDetailsModal(QDialog):
|
|||
|
||||
if not missing_tracks:
|
||||
QMessageBox.information(self, "Analysis Complete",
|
||||
"All tracks already exist in Plex library!\nNo downloads needed.")
|
||||
f"All tracks already exist in {server_name} library!\nNo downloads needed.")
|
||||
return
|
||||
|
||||
# Show results to user
|
||||
message = f"Analysis complete!\n\n"
|
||||
message += f"Tracks already in Plex: {len(existing_tracks)}\n"
|
||||
# Get server name for display
|
||||
try:
|
||||
from config.settings import config_manager
|
||||
active_server = config_manager.get_active_media_server()
|
||||
server_name = active_server.title() if active_server else "Plex"
|
||||
except:
|
||||
server_name = "Plex"
|
||||
|
||||
message += f"Tracks already in {server_name}: {len(existing_tracks)}\n"
|
||||
message += f"Tracks to download: {len(missing_tracks)}\n\n"
|
||||
message += "Ready to start downloading missing tracks?"
|
||||
|
||||
|
|
@ -3086,11 +3103,12 @@ class SyncPage(QWidget):
|
|||
sync_activity = pyqtSignal(str, str, str, str) # icon, title, subtitle, time
|
||||
database_updated_externally = pyqtSignal()
|
||||
|
||||
def __init__(self, spotify_client=None, plex_client=None, soulseek_client=None, downloads_page=None, jellyfin_client=None, tidal_client=None, parent=None):
|
||||
def __init__(self, spotify_client=None, plex_client=None, soulseek_client=None, downloads_page=None, jellyfin_client=None, navidrome_client=None, tidal_client=None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.spotify_client = spotify_client
|
||||
self.plex_client = plex_client
|
||||
self.jellyfin_client = jellyfin_client
|
||||
self.navidrome_client = navidrome_client
|
||||
self.soulseek_client = soulseek_client
|
||||
self.tidal_client = tidal_client or TidalClient()
|
||||
self.downloads_page = downloads_page
|
||||
|
|
@ -3293,7 +3311,7 @@ class SyncPage(QWidget):
|
|||
if playlist.id in self.active_sync_workers:
|
||||
# Already syncing
|
||||
return False
|
||||
|
||||
|
||||
# Create sync service if not available
|
||||
if not hasattr(self, 'sync_service'):
|
||||
from services.sync_service import PlaylistSyncService
|
||||
|
|
@ -3301,7 +3319,8 @@ class SyncPage(QWidget):
|
|||
self.spotify_client,
|
||||
self.plex_client,
|
||||
self.soulseek_client,
|
||||
getattr(self, 'jellyfin_client', None)
|
||||
getattr(self, 'jellyfin_client', None),
|
||||
getattr(self, 'navidrome_client', None)
|
||||
)
|
||||
|
||||
# Create sync worker
|
||||
|
|
@ -3355,7 +3374,8 @@ class SyncPage(QWidget):
|
|||
self.spotify_client,
|
||||
self.plex_client,
|
||||
self.soulseek_client,
|
||||
getattr(self, 'jellyfin_client', None)
|
||||
getattr(self, 'jellyfin_client', None),
|
||||
getattr(self, 'navidrome_client', None)
|
||||
)
|
||||
|
||||
# Create sync worker for sequential sync
|
||||
|
|
@ -4059,7 +4079,15 @@ class SyncPage(QWidget):
|
|||
title_label.setStyleSheet("color: #ffffff;")
|
||||
|
||||
# Subtitle
|
||||
subtitle_label = QLabel("Synchronize your Spotify playlists with Plex")
|
||||
# Get active server name for subtitle
|
||||
try:
|
||||
from config.settings import config_manager
|
||||
active_server = config_manager.get_active_media_server()
|
||||
server_name = active_server.title() if active_server else "Plex"
|
||||
except:
|
||||
server_name = "Plex"
|
||||
|
||||
subtitle_label = QLabel(f"Synchronize your Spotify playlists with {server_name}")
|
||||
subtitle_label.setFont(QFont("Arial", 14))
|
||||
subtitle_label.setStyleSheet("color: #b3b3b3;")
|
||||
|
||||
|
|
@ -8100,7 +8128,15 @@ class DownloadMissingTracksModal(QDialog):
|
|||
# The modal now stays open.
|
||||
# The process_finished signal is still emitted to unlock the main UI.
|
||||
self.process_finished.emit()
|
||||
QMessageBox.information(self, "Analysis Complete", "All tracks already exist in Plex! No downloads needed.")
|
||||
# Get server name for message
|
||||
try:
|
||||
from config.settings import config_manager
|
||||
active_server = config_manager.get_active_media_server()
|
||||
server_name = active_server.title() if active_server else "Plex"
|
||||
except:
|
||||
server_name = "Plex"
|
||||
|
||||
QMessageBox.information(self, "Analysis Complete", f"All tracks already exist in {server_name}! No downloads needed.")
|
||||
|
||||
def on_analysis_failed(self, error_message):
|
||||
print(f"❌ Analysis failed: {error_message}")
|
||||
|
|
|
|||
|
|
@ -1308,7 +1308,12 @@ class ModernSidebar(QWidget):
|
|||
# Dynamic media server status - determine which server is active
|
||||
from config.settings import config_manager
|
||||
active_server = config_manager.get_active_media_server()
|
||||
server_name = "Plex" if active_server == "plex" else "Jellyfin"
|
||||
server_name_map = {
|
||||
'plex': 'Plex',
|
||||
'jellyfin': 'Jellyfin',
|
||||
'navidrome': 'Navidrome'
|
||||
}
|
||||
server_name = server_name_map.get(active_server, 'Jellyfin')
|
||||
self.media_server_status = StatusIndicator(server_name)
|
||||
|
||||
self.soulseek_status = StatusIndicator("Soulseek")
|
||||
|
|
@ -1333,6 +1338,7 @@ class ModernSidebar(QWidget):
|
|||
"spotify": self.spotify_status,
|
||||
"plex": self.media_server_status,
|
||||
"jellyfin": self.media_server_status,
|
||||
"navidrome": self.media_server_status,
|
||||
"soulseek": self.soulseek_status
|
||||
}
|
||||
|
||||
|
|
@ -1341,7 +1347,12 @@ class ModernSidebar(QWidget):
|
|||
|
||||
def update_media_server_name(self, server_type: str):
|
||||
"""Update the media server status indicator name"""
|
||||
server_name = "Plex" if server_type == "plex" else "Jellyfin"
|
||||
server_name_map = {
|
||||
'plex': 'Plex',
|
||||
'jellyfin': 'Jellyfin',
|
||||
'navidrome': 'Navidrome'
|
||||
}
|
||||
server_name = server_name_map.get(server_type, 'Jellyfin')
|
||||
if hasattr(self, 'media_server_status'):
|
||||
self.media_server_status.update_name(server_name)
|
||||
|
||||
|
|
|
|||
|
|
@ -6967,8 +6967,8 @@ async function loadDashboardData() {
|
|||
metadataButton.addEventListener('click', handleMetadataUpdateButtonClick);
|
||||
}
|
||||
|
||||
// Check active media server and hide metadata updater if Jellyfin
|
||||
await checkAndHideMetadataUpdaterForJellyfin();
|
||||
// Check active media server and hide metadata updater if not Plex
|
||||
await checkAndHideMetadataUpdaterForNonPlex();
|
||||
|
||||
// Check for ongoing metadata update and restore state
|
||||
await checkAndRestoreMetadataUpdateState();
|
||||
|
|
@ -12968,24 +12968,24 @@ function updateMetadataProgressUI(status) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Check active media server and hide metadata updater if Jellyfin
|
||||
* Check active media server and hide metadata updater if not Plex
|
||||
*/
|
||||
async function checkAndHideMetadataUpdaterForJellyfin() {
|
||||
async function checkAndHideMetadataUpdaterForNonPlex() {
|
||||
try {
|
||||
const response = await fetch('/api/active-media-server');
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
if (data.success) {
|
||||
const metadataCard = document.getElementById('metadata-updater-card');
|
||||
if (metadataCard) {
|
||||
if (data.active_server === 'jellyfin') {
|
||||
// Hide metadata updater for Jellyfin (same as dashboard.py behavior)
|
||||
metadataCard.style.display = 'none';
|
||||
console.log('Metadata updater hidden: Jellyfin is active server');
|
||||
} else {
|
||||
// Show metadata updater for Plex
|
||||
if (data.active_server === 'plex') {
|
||||
// Show metadata updater only for Plex
|
||||
metadataCard.style.display = 'block';
|
||||
console.log('Metadata updater shown: Plex is active server');
|
||||
} else {
|
||||
// Hide metadata updater for Jellyfin and Navidrome (same as dashboard.py behavior)
|
||||
metadataCard.style.display = 'none';
|
||||
console.log(`Metadata updater hidden: ${data.active_server} is active server`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue