add navidrome to settings
This commit is contained in:
parent
90d99d2a66
commit
0f64d17bf6
6 changed files with 198 additions and 12 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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...');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue