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",
|
"api_key": "JELLYFIN_API_KEY",
|
||||||
"auto_detect": true
|
"auto_detect": true
|
||||||
},
|
},
|
||||||
|
"navidrome": {
|
||||||
|
"base_url": "http://localhost:4533",
|
||||||
|
"username": "NAVIDROME_USERNAME",
|
||||||
|
"password": "NAVIDROME_PASSWORD",
|
||||||
|
"auto_detect": true
|
||||||
|
},
|
||||||
"soulseek": {
|
"soulseek": {
|
||||||
"slskd_url": "http://host.docker.internal:5030",
|
"slskd_url": "http://host.docker.internal:5030",
|
||||||
"api_key": "SoulseekAPIKey",
|
"api_key": "SoulseekAPIKey",
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,9 @@ class ConfigManager:
|
||||||
def get_jellyfin_config(self) -> Dict[str, str]:
|
def get_jellyfin_config(self) -> Dict[str, str]:
|
||||||
return self.get('jellyfin', {})
|
return self.get('jellyfin', {})
|
||||||
|
|
||||||
|
def get_navidrome_config(self) -> Dict[str, str]:
|
||||||
|
return self.get('navidrome', {})
|
||||||
|
|
||||||
def get_soulseek_config(self) -> Dict[str, str]:
|
def get_soulseek_config(self) -> Dict[str, str]:
|
||||||
return self.get('soulseek', {})
|
return self.get('soulseek', {})
|
||||||
|
|
||||||
|
|
@ -83,8 +86,8 @@ class ConfigManager:
|
||||||
return self.get('active_media_server', 'plex')
|
return self.get('active_media_server', 'plex')
|
||||||
|
|
||||||
def set_active_media_server(self, server: str):
|
def set_active_media_server(self, server: str):
|
||||||
"""Set the active media server (plex or jellyfin)"""
|
"""Set the active media server (plex, jellyfin, or navidrome)"""
|
||||||
if server not in ['plex', 'jellyfin']:
|
if server not in ['plex', 'jellyfin', 'navidrome']:
|
||||||
raise ValueError(f"Invalid media server: {server}")
|
raise ValueError(f"Invalid media server: {server}")
|
||||||
self.set('active_media_server', server)
|
self.set('active_media_server', server)
|
||||||
|
|
||||||
|
|
@ -95,6 +98,8 @@ class ConfigManager:
|
||||||
return self.get_plex_config()
|
return self.get_plex_config()
|
||||||
elif active_server == 'jellyfin':
|
elif active_server == 'jellyfin':
|
||||||
return self.get_jellyfin_config()
|
return self.get_jellyfin_config()
|
||||||
|
elif active_server == 'navidrome':
|
||||||
|
return self.get_navidrome_config()
|
||||||
else:
|
else:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
@ -111,6 +116,9 @@ class ConfigManager:
|
||||||
elif active_server == 'jellyfin':
|
elif active_server == 'jellyfin':
|
||||||
jellyfin = self.get_jellyfin_config()
|
jellyfin = self.get_jellyfin_config()
|
||||||
media_server_configured = bool(jellyfin.get('base_url')) and bool(jellyfin.get('api_key'))
|
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 (
|
return (
|
||||||
bool(spotify.get('client_id')) and
|
bool(spotify.get('client_id')) and
|
||||||
|
|
@ -127,9 +135,10 @@ class ConfigManager:
|
||||||
'soulseek': bool(self.get('soulseek.slskd_url'))
|
'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['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['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
|
validation['active_media_server'] = active_server
|
||||||
|
|
||||||
return validation
|
return validation
|
||||||
|
|
|
||||||
|
|
@ -1146,6 +1146,48 @@ def run_service_test(service, test_config):
|
||||||
return True, f"Successfully connected to Jellyfin server: {server_name}"
|
return True, f"Successfully connected to Jellyfin server: {server_name}"
|
||||||
else:
|
else:
|
||||||
return False, "Could not connect to Jellyfin. Check URL and API Key."
|
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":
|
elif service == "soulseek":
|
||||||
temp_client = SoulseekClient()
|
temp_client = SoulseekClient()
|
||||||
async def check():
|
async def check():
|
||||||
|
|
@ -1279,6 +1321,38 @@ def run_detection(server_type):
|
||||||
pass
|
pass
|
||||||
return None
|
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
|
||||||
|
|
||||||
try:
|
try:
|
||||||
network_addr, netmask, local_ip, network = get_network_info()
|
network_addr, netmask, local_ip, network = get_network_info()
|
||||||
|
|
||||||
|
|
@ -1286,6 +1360,7 @@ def run_detection(server_type):
|
||||||
test_functions = {
|
test_functions = {
|
||||||
'plex': test_plex_server,
|
'plex': test_plex_server,
|
||||||
'jellyfin': test_jellyfin_server,
|
'jellyfin': test_jellyfin_server,
|
||||||
|
'navidrome': test_navidrome_server,
|
||||||
'slskd': test_slskd_server
|
'slskd': test_slskd_server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1414,6 +1489,11 @@ def get_status():
|
||||||
# Create fresh client to ensure Docker URL resolution
|
# Create fresh client to ensure Docker URL resolution
|
||||||
temp_jellyfin_client = JellyfinClient()
|
temp_jellyfin_client = JellyfinClient()
|
||||||
media_server_status = temp_jellyfin_client.is_connected()
|
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
|
media_server_response_time = (time.time() - media_server_start) * 1000
|
||||||
|
|
||||||
# Test Soulseek (just check if configured, no network test)
|
# Test Soulseek (just check if configured, no network test)
|
||||||
|
|
@ -1592,7 +1672,7 @@ def handle_settings():
|
||||||
if 'active_media_server' in new_settings:
|
if 'active_media_server' in new_settings:
|
||||||
config_manager.set_active_media_server(new_settings['active_media_server'])
|
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:
|
if service in new_settings:
|
||||||
for key, value in new_settings[service].items():
|
for key, value in new_settings[service].items():
|
||||||
config_manager.set(f'{service}.{key}', value)
|
config_manager.set(f'{service}.{key}', value)
|
||||||
|
|
|
||||||
|
|
@ -721,8 +721,16 @@
|
||||||
|
|
||||||
<!-- Server Toggle Buttons -->
|
<!-- Server Toggle Buttons -->
|
||||||
<div class="server-toggle-container">
|
<div class="server-toggle-container">
|
||||||
<button class="server-toggle-btn active" id="plex-toggle" onclick="toggleServer('plex')">Plex</button>
|
<button class="server-toggle-btn active" id="plex-toggle" onclick="toggleServer('plex')">
|
||||||
<button class="server-toggle-btn" id="jellyfin-toggle" onclick="toggleServer('jellyfin')">Jellyfin</button>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Plex Settings -->
|
<!-- Plex Settings -->
|
||||||
|
|
@ -757,6 +765,26 @@
|
||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Server Test Button -->
|
||||||
<div class="server-test-section">
|
<div class="server-test-section">
|
||||||
<button class="test-button server-test-btn" onclick="testConnection('server')">Test Server</button>
|
<button class="test-button server-test-btn" onclick="testConnection('server')">Test Server</button>
|
||||||
|
|
|
||||||
|
|
@ -1373,6 +1373,11 @@ async function loadSettingsData() {
|
||||||
document.getElementById('jellyfin-url').value = settings.jellyfin?.base_url || '';
|
document.getElementById('jellyfin-url').value = settings.jellyfin?.base_url || '';
|
||||||
document.getElementById('jellyfin-api-key').value = settings.jellyfin?.api_key || '';
|
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
|
// Set active server and toggle visibility
|
||||||
const activeServer = settings.active_media_server || 'plex';
|
const activeServer = settings.active_media_server || 'plex';
|
||||||
toggleServer(activeServer);
|
toggleServer(activeServer);
|
||||||
|
|
@ -1424,16 +1429,23 @@ function toggleServer(serverType) {
|
||||||
// Update toggle buttons
|
// Update toggle buttons
|
||||||
document.getElementById('plex-toggle').classList.remove('active');
|
document.getElementById('plex-toggle').classList.remove('active');
|
||||||
document.getElementById('jellyfin-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');
|
document.getElementById(`${serverType}-toggle`).classList.add('active');
|
||||||
|
|
||||||
// Show/hide server containers
|
// Show/hide server containers
|
||||||
document.getElementById('plex-container').classList.toggle('hidden', serverType !== 'plex');
|
document.getElementById('plex-container').classList.toggle('hidden', serverType !== 'plex');
|
||||||
document.getElementById('jellyfin-container').classList.toggle('hidden', serverType !== 'jellyfin');
|
document.getElementById('jellyfin-container').classList.toggle('hidden', serverType !== 'jellyfin');
|
||||||
|
document.getElementById('navidrome-container').classList.toggle('hidden', serverType !== 'navidrome');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSettings() {
|
async function saveSettings() {
|
||||||
// Determine active server from toggle buttons
|
// 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 = {
|
const settings = {
|
||||||
active_media_server: activeServer,
|
active_media_server: activeServer,
|
||||||
|
|
@ -1455,6 +1467,11 @@ async function saveSettings() {
|
||||||
base_url: document.getElementById('jellyfin-url').value,
|
base_url: document.getElementById('jellyfin-url').value,
|
||||||
api_key: document.getElementById('jellyfin-api-key').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: {
|
soulseek: {
|
||||||
slskd_url: document.getElementById('soulseek-url').value,
|
slskd_url: document.getElementById('soulseek-url').value,
|
||||||
api_key: document.getElementById('soulseek-api-key').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() {
|
async function autoDetectSlskd() {
|
||||||
try {
|
try {
|
||||||
showLoadingOverlay('Auto-detecting Soulseek (slskd) server...');
|
showLoadingOverlay('Auto-detecting Soulseek (slskd) server...');
|
||||||
|
|
|
||||||
|
|
@ -1058,6 +1058,17 @@ body {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
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 {
|
.server-toggle-btn.active {
|
||||||
|
|
@ -1072,10 +1083,18 @@ body {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.server-toggle-btn:hover .server-logo {
|
||||||
|
opacity: 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
.server-toggle-btn.active:hover {
|
.server-toggle-btn.active:hover {
|
||||||
background: rgba(29, 185, 84, 0.3);
|
background: rgba(29, 185, 84, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.server-toggle-btn.active .server-logo {
|
||||||
|
opacity: 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
.server-config-container {
|
.server-config-container {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue