add navidrome to settings

This commit is contained in:
Broque Thomas 2025-09-21 14:10:17 -07:00
parent 90d99d2a66
commit 0f64d17bf6
6 changed files with 198 additions and 12 deletions

View file

@ -20,6 +20,12 @@
"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://host.docker.internal:5030",
"api_key": "SoulseekAPIKey",

View file

@ -66,7 +66,10 @@ class ConfigManager:
def get_jellyfin_config(self) -> Dict[str, str]:
return self.get('jellyfin', {})
def get_navidrome_config(self) -> Dict[str, str]:
return self.get('navidrome', {})
def get_soulseek_config(self) -> Dict[str, str]:
return self.get('soulseek', {})
@ -83,8 +86,8 @@ class ConfigManager:
return self.get('active_media_server', 'plex')
def set_active_media_server(self, server: str):
"""Set the active media server (plex or jellyfin)"""
if server not in ['plex', 'jellyfin']:
"""Set the active media server (plex, jellyfin, or navidrome)"""
if server not in ['plex', 'jellyfin', 'navidrome']:
raise ValueError(f"Invalid media server: {server}")
self.set('active_media_server', server)
@ -95,6 +98,8 @@ class ConfigManager:
return self.get_plex_config()
elif active_server == 'jellyfin':
return self.get_jellyfin_config()
elif active_server == 'navidrome':
return self.get_navidrome_config()
else:
return {}
@ -111,6 +116,9 @@ class ConfigManager:
elif active_server == 'jellyfin':
jellyfin = self.get_jellyfin_config()
media_server_configured = bool(jellyfin.get('base_url')) and bool(jellyfin.get('api_key'))
elif active_server == 'navidrome':
navidrome = self.get_navidrome_config()
media_server_configured = bool(navidrome.get('base_url')) and bool(navidrome.get('username')) and bool(navidrome.get('password'))
return (
bool(spotify.get('client_id')) and
@ -127,9 +135,10 @@ class ConfigManager:
'soulseek': bool(self.get('soulseek.slskd_url'))
}
# Validate both server types but mark active one
# Validate all server types but mark active one
validation['plex'] = bool(self.get('plex.base_url')) and bool(self.get('plex.token'))
validation['jellyfin'] = bool(self.get('jellyfin.base_url')) and bool(self.get('jellyfin.api_key'))
validation['navidrome'] = bool(self.get('navidrome.base_url')) and bool(self.get('navidrome.username')) and bool(self.get('navidrome.password'))
validation['active_media_server'] = active_server
return validation

View file

@ -1146,6 +1146,48 @@ def run_service_test(service, test_config):
return True, f"Successfully connected to Jellyfin server: {server_name}"
else:
return False, "Could not connect to Jellyfin. Check URL and API Key."
elif service == "navidrome":
# Test Navidrome connection using Subsonic API
base_url = test_config.get('base_url', '')
username = test_config.get('username', '')
password = test_config.get('password', '')
if not all([base_url, username, password]):
return False, "Missing Navidrome URL, username, or password."
try:
import hashlib
import random
import string
# Generate salt and token for Subsonic API authentication
salt = ''.join(random.choices(string.ascii_letters + string.digits, k=6))
token = hashlib.md5((password + salt).encode()).hexdigest()
# Test ping endpoint
url = f"{base_url.rstrip('/')}/rest/ping"
response = requests.get(url, params={
'u': username,
't': token,
's': salt,
'v': '1.16.1',
'c': 'soulsync',
'f': 'json'
}, timeout=5)
if response.status_code == 200:
data = response.json()
if data.get('subsonic-response', {}).get('status') == 'ok':
server_version = data.get('subsonic-response', {}).get('version', 'Unknown')
return True, f"Successfully connected to Navidrome server (v{server_version})"
else:
error = data.get('subsonic-response', {}).get('error', {})
return False, f"Navidrome authentication failed: {error.get('message', 'Unknown error')}"
else:
return False, f"Could not connect to Navidrome server (HTTP {response.status_code})"
except Exception as e:
return False, f"Navidrome connection error: {str(e)}"
elif service == "soulseek":
temp_client = SoulseekClient()
async def check():
@ -1274,7 +1316,39 @@ def run_detection(server_type):
# 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
def test_navidrome_server(ip, port=4533):
"""Test if a Navidrome server is running at the given IP and port"""
try:
# Try Navidrome's ping endpoint (part of Subsonic API)
url = f"http://{ip}:{port}/rest/ping"
response = requests.get(url, timeout=2, params={
'u': 'test', # Dummy username for ping test
'v': '1.16.1', # API version
'c': 'soulsync', # Client name
'f': 'json' # Response format
})
# Navidrome should respond even with invalid credentials for ping
if response.status_code in [200, 401, 403]:
try:
data = response.json()
# Check for Subsonic/Navidrome API response structure
if 'subsonic-response' in data:
return f"http://{ip}:{port}"
except:
pass
# Also try the web interface
web_url = f"http://{ip}:{port}/"
web_response = requests.get(web_url, timeout=2)
if web_response.status_code == 200 and 'navidrome' in web_response.text.lower():
return f"http://{ip}:{port}"
except:
pass
return None
@ -1286,6 +1360,7 @@ def run_detection(server_type):
test_functions = {
'plex': test_plex_server,
'jellyfin': test_jellyfin_server,
'navidrome': test_navidrome_server,
'slskd': test_slskd_server
}
@ -1414,6 +1489,11 @@ def get_status():
# Create fresh client to ensure Docker URL resolution
temp_jellyfin_client = JellyfinClient()
media_server_status = temp_jellyfin_client.is_connected()
elif active_server == "navidrome":
# Test Navidrome connection
navidrome_config = config_manager.get('navidrome', {})
success, _ = run_service_test('navidrome', navidrome_config)
media_server_status = success
media_server_response_time = (time.time() - media_server_start) * 1000
# Test Soulseek (just check if configured, no network test)
@ -1592,7 +1672,7 @@ def handle_settings():
if 'active_media_server' in new_settings:
config_manager.set_active_media_server(new_settings['active_media_server'])
for service in ['spotify', 'plex', 'jellyfin', 'soulseek', 'settings', 'database', 'metadata_enhancement', 'playlist_sync', 'tidal']:
for service in ['spotify', 'plex', 'jellyfin', 'navidrome', 'soulseek', 'settings', 'database', 'metadata_enhancement', 'playlist_sync', 'tidal']:
if service in new_settings:
for key, value in new_settings[service].items():
config_manager.set(f'{service}.{key}', value)

View file

@ -721,8 +721,16 @@
<!-- Server Toggle Buttons -->
<div class="server-toggle-container">
<button class="server-toggle-btn active" id="plex-toggle" onclick="toggleServer('plex')">Plex</button>
<button class="server-toggle-btn" id="jellyfin-toggle" onclick="toggleServer('jellyfin')">Jellyfin</button>
<button class="server-toggle-btn active" id="plex-toggle" onclick="toggleServer('plex')">
<img src="https://www.plex.tv/wp-content/themes/plex/assets/img/plex-logo.svg" alt="Plex" class="server-logo">
</button>
<button class="server-toggle-btn" id="jellyfin-toggle" onclick="toggleServer('jellyfin')">
<img src="https://jellyfin.org/images/logo.svg" alt="Jellyfin" class="server-logo">
</button>
<button class="server-toggle-btn" id="navidrome-toggle" onclick="toggleServer('navidrome')">
<img src="https://tweakers.net/ext/i/2007323764.png" alt="Navidrome" class="server-logo">
Navidrome
</button>
</div>
<!-- Plex Settings -->
@ -756,7 +764,27 @@
<button class="test-button" onclick="testConnection('jellyfin')">Test</button>
</div>
</div>
<!-- Navidrome Settings -->
<div class="server-config-container hidden" id="navidrome-container">
<div class="form-group">
<label>Navidrome Server URL:</label>
<input type="url" id="navidrome-url" placeholder="http://localhost:4533">
</div>
<div class="form-group">
<label>Username:</label>
<input type="text" id="navidrome-username" placeholder="Username">
</div>
<div class="form-group">
<label>Password:</label>
<input type="password" id="navidrome-password" placeholder="Password">
</div>
<div class="form-actions">
<button class="detect-button" onclick="autoDetectNavidrome()">Auto-detect</button>
<button class="test-button" onclick="testConnection('navidrome')">Test</button>
</div>
</div>
<!-- Server Test Button -->
<div class="server-test-section">
<button class="test-button server-test-btn" onclick="testConnection('server')">Test Server</button>

View file

@ -1372,7 +1372,12 @@ async function loadSettingsData() {
// Populate Jellyfin settings
document.getElementById('jellyfin-url').value = settings.jellyfin?.base_url || '';
document.getElementById('jellyfin-api-key').value = settings.jellyfin?.api_key || '';
// Populate Navidrome settings
document.getElementById('navidrome-url').value = settings.navidrome?.base_url || '';
document.getElementById('navidrome-username').value = settings.navidrome?.username || '';
document.getElementById('navidrome-password').value = settings.navidrome?.password || '';
// Set active server and toggle visibility
const activeServer = settings.active_media_server || 'plex';
toggleServer(activeServer);
@ -1424,16 +1429,23 @@ function toggleServer(serverType) {
// Update toggle buttons
document.getElementById('plex-toggle').classList.remove('active');
document.getElementById('jellyfin-toggle').classList.remove('active');
document.getElementById('navidrome-toggle').classList.remove('active');
document.getElementById(`${serverType}-toggle`).classList.add('active');
// Show/hide server containers
document.getElementById('plex-container').classList.toggle('hidden', serverType !== 'plex');
document.getElementById('jellyfin-container').classList.toggle('hidden', serverType !== 'jellyfin');
document.getElementById('navidrome-container').classList.toggle('hidden', serverType !== 'navidrome');
}
async function saveSettings() {
// Determine active server from toggle buttons
const activeServer = document.getElementById('plex-toggle').classList.contains('active') ? 'plex' : 'jellyfin';
let activeServer = 'plex';
if (document.getElementById('jellyfin-toggle').classList.contains('active')) {
activeServer = 'jellyfin';
} else if (document.getElementById('navidrome-toggle').classList.contains('active')) {
activeServer = 'navidrome';
}
const settings = {
active_media_server: activeServer,
@ -1455,6 +1467,11 @@ async function saveSettings() {
base_url: document.getElementById('jellyfin-url').value,
api_key: document.getElementById('jellyfin-api-key').value
},
navidrome: {
base_url: document.getElementById('navidrome-url').value,
username: document.getElementById('navidrome-username').value,
password: document.getElementById('navidrome-password').value
},
soulseek: {
slskd_url: document.getElementById('soulseek-url').value,
api_key: document.getElementById('soulseek-api-key').value,
@ -1608,6 +1625,33 @@ async function autoDetectJellyfin() {
}
}
async function autoDetectNavidrome() {
try {
showLoadingOverlay('Auto-detecting Navidrome server...');
const response = await fetch('/api/detect-media-server', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ server_type: 'navidrome' })
});
const result = await response.json();
if (result.success) {
document.getElementById('navidrome-url').value = result.found_url;
showToast(`Navidrome server detected: ${result.found_url}`, 'success');
} else {
showToast(result.error, 'error');
}
} catch (error) {
console.error('Error auto-detecting Navidrome:', error);
showToast('Failed to auto-detect Navidrome server', 'error');
} finally {
hideLoadingOverlay();
}
}
async function autoDetectSlskd() {
try {
showLoadingOverlay('Auto-detecting Soulseek (slskd) server...');

View file

@ -1058,6 +1058,17 @@ body {
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
justify-content: center;
flex-direction: column;
}
.server-logo {
width: 100%;
opacity: 0.8;
transition: opacity 0.2s ease;
}
.server-toggle-btn.active {
@ -1072,10 +1083,18 @@ body {
color: #ffffff;
}
.server-toggle-btn:hover .server-logo {
opacity: 1.0;
}
.server-toggle-btn.active:hover {
background: rgba(29, 185, 84, 0.3);
}
.server-toggle-btn.active .server-logo {
opacity: 1.0;
}
.server-config-container {
padding-top: 8px;
}