diff --git a/core/navidrome_client.py b/core/navidrome_client.py index 1789791c..7a14cd89 100644 --- a/core/navidrome_client.py +++ b/core/navidrome_client.py @@ -146,6 +146,7 @@ class NavidromeClient: self.base_url: Optional[str] = None self.username: Optional[str] = None self.password: Optional[str] = None + self.music_folder_id: Optional[str] = None self._connection_attempted = False self._is_connecting = False @@ -161,6 +162,61 @@ class NavidromeClient: """Set callback function for progress updates""" self._progress_callback = callback + def reload_config(self): + """Reset connection state so next ensure_connection() re-reads config.""" + self.base_url = None + self.username = None + self.password = None + self.music_folder_id = None + self._connection_attempted = False + self._artist_cache.clear() + self._album_cache.clear() + self._track_cache.clear() + logger.info("🔄 Navidrome client config reset — will reconnect with new settings") + + def get_music_folders(self) -> list: + """Get available music folders from Navidrome.""" + if not self.ensure_connection(): + return [] + try: + response = self._make_request('getMusicFolders') + if not response: + return [] + folders_data = response.get('musicFolders', {}) + folder_list = folders_data.get('musicFolder', []) + if isinstance(folder_list, dict): + folder_list = [folder_list] + return [{'title': f.get('name', 'Unknown'), 'key': str(f.get('id', ''))} for f in folder_list] + except Exception as e: + logger.error(f"Error getting music folders: {e}") + return [] + + def set_music_folder_by_name(self, folder_name: str) -> bool: + """Set the active music folder by name.""" + try: + folders = self.get_music_folders() + for folder in folders: + if folder['title'] == folder_name: + self.music_folder_id = folder['key'] + logger.info(f"Set music folder to: {folder_name} (ID: {self.music_folder_id})") + from database.music_database import MusicDatabase + db = MusicDatabase() + db.set_preference('navidrome_music_folder', folder_name) + return True + # If folder_name is empty, clear the selection + if not folder_name: + self.music_folder_id = None + from database.music_database import MusicDatabase + db = MusicDatabase() + db.set_preference('navidrome_music_folder', '') + logger.info("Cleared music folder selection — will use all libraries") + return True + logger.warning(f"Music folder '{folder_name}' not found") + return False + except Exception as e: + logger.error(f"Error setting music folder: {e}") + return False + def ensure_connection(self) -> bool: """Ensure connection to Navidrome server with lazy initialization.""" if self._connection_attempted: @@ -199,6 +255,21 @@ class NavidromeClient: if response and response.get('status') == 'ok': server_version = response.get('version', 'Unknown') logger.info(f"Successfully connected to Navidrome server version: {server_version}") + + # Restore saved music folder preference + try: + from database.music_database import MusicDatabase + db = MusicDatabase() + saved_folder = db.get_preference('navidrome_music_folder') + if saved_folder: + folders = self.get_music_folders() + for folder in folders: + if folder['title'] == saved_folder: + self.music_folder_id = folder['key'] + logger.info(f"Restored music folder preference: {saved_folder} (ID: {self.music_folder_id})") + break + except Exception as e: + logger.warning(f"Could not restore music folder preference: {e}") else: logger.error(f"Failed to connect to Navidrome server at {self.base_url}/rest/ping — check URL and network connectivity") self.base_url = None @@ -284,7 +355,10 @@ class NavidromeClient: if self._progress_callback: self._progress_callback("Fetching artists from Navidrome...") - response = self._make_request('getArtists') + params = {} + if self.music_folder_id: + params['musicFolderId'] = self.music_folder_id + response = self._make_request('getArtists', params if params else None) if not response: return [] @@ -371,11 +445,14 @@ class NavidromeClient: offset = 0 while len(albums) < limit: - response = self._make_request('getAlbumList2', { + params = { 'type': 'newest', 'size': page_size, 'offset': offset - }) + } + if self.music_folder_id: + params['musicFolderId'] = self.music_folder_id + response = self._make_request('getAlbumList2', params) if not response: break @@ -819,12 +896,15 @@ class NavidromeClient: try: # Use Subsonic search3 API for music search query = f"{artist} {title}".strip() - response = self._make_request('search3', { + params = { 'query': query, 'songCount': limit, 'artistCount': 0, 'albumCount': 0 - }) + } + if self.music_folder_id: + params['musicFolderId'] = self.music_folder_id + response = self._make_request('search3', params) if not response: return [] diff --git a/web_server.py b/web_server.py index 6b0a1c03..37ce5fd5 100644 --- a/web_server.py +++ b/web_server.py @@ -2531,6 +2531,7 @@ def handle_settings(): spotify_client.reload_config() plex_client.server = None jellyfin_client.reload_config() + navidrome_client.reload_config() # Reload orchestrator settings (download source mode, hybrid_primary, etc.) soulseek_client.reload_settings() # FIX: Re-instantiate the global tidal_client to pick up new settings @@ -3074,6 +3075,56 @@ def select_jellyfin_music_library(): logger.error(f"Error setting Jellyfin music library: {e}") return jsonify({"success": False, "error": str(e)}), 500 +@app.route('/api/navidrome/music-folders', methods=['GET']) +def get_navidrome_music_folders(): + """Get list of available music folders from Navidrome""" + try: + folders = navidrome_client.get_music_folders() + + from database.music_database import MusicDatabase + db = MusicDatabase() + selected_folder = db.get_preference('navidrome_music_folder') + + current_folder = None + if navidrome_client.music_folder_id: + for f in folders: + if f['key'] == navidrome_client.music_folder_id: + current_folder = f['title'] + break + + return jsonify({ + "success": True, + "folders": folders, + "selected": selected_folder, + "current": current_folder + }) + except Exception as e: + logger.error(f"Error getting Navidrome music folders: {e}") + return jsonify({"success": False, "error": str(e)}), 500 + +@app.route('/api/navidrome/select-music-folder', methods=['POST']) +def select_navidrome_music_folder(): + """Set the active Navidrome music folder""" + try: + data = request.get_json() + folder_name = data.get('folder_name', '') + + success = navidrome_client.set_music_folder_by_name(folder_name) + + if success: + if folder_name: + add_activity_item("📚", "Library Selected", f"Navidrome music folder set to: {folder_name}", "Now") + return jsonify({"success": True, "message": f"Music folder set to: {folder_name}"}) + else: + add_activity_item("📚", "Library Selection Cleared", "Navidrome will use all libraries", "Now") + return jsonify({"success": True, "message": "Music folder selection cleared — using all libraries"}) + else: + return jsonify({"success": False, "error": f"Folder '{folder_name}' not found"}), 404 + + except Exception as e: + logger.error(f"Error setting Navidrome music folder: {e}") + return jsonify({"success": False, "error": str(e)}), 500 + # =============================== # == QUALITY PROFILE API == # =============================== diff --git a/webui/index.html b/webui/index.html index 24080114..4e62dd3c 100644 --- a/webui/index.html +++ b/webui/index.html @@ -3012,6 +3012,14 @@ +
diff --git a/webui/static/script.js b/webui/static/script.js index 43a36010..21b0675b 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -1808,6 +1808,11 @@ async function loadSettingsData() { loadJellyfinUsers().then(() => loadJellyfinMusicLibraries()); } + // Load Navidrome music folders if Navidrome is the active server + if (activeServer === 'navidrome') { + loadNavidromeMusicFolders(); + } + // Populate Soulseek settings document.getElementById('soulseek-url').value = settings.soulseek?.slskd_url || ''; document.getElementById('soulseek-api-key').value = settings.soulseek?.api_key || ''; @@ -1968,6 +1973,11 @@ function toggleServer(serverType) { loadJellyfinUsers().then(() => loadJellyfinMusicLibraries()); } + // Load Navidrome music folders when switching to Navidrome + if (serverType === 'navidrome') { + loadNavidromeMusicFolders(); + } + // Auto-save after server toggle change debouncedAutoSaveSettings(); } @@ -2589,6 +2599,8 @@ async function testConnection(service) { loadPlexMusicLibraries(); } else if (service === 'jellyfin') { loadJellyfinUsers().then(() => loadJellyfinMusicLibraries()); + } else if (service === 'navidrome') { + loadNavidromeMusicFolders(); } } else { showToast(`${service} connection failed: ${result.error}`, 'error'); @@ -32712,6 +32724,66 @@ async function selectJellyfinLibrary() { } } +// ============ Navidrome Music Folder Selection ============ + +async function loadNavidromeMusicFolders() { + try { + const response = await fetch('/api/navidrome/music-folders'); + const data = await response.json(); + + if (data.success && data.folders && data.folders.length > 0) { + const selector = document.getElementById('navidrome-music-folder'); + const container = document.getElementById('navidrome-folder-selector-container'); + + selector.innerHTML = ''; + + data.folders.forEach(folder => { + const option = document.createElement('option'); + option.value = folder.title; + option.textContent = folder.title; + + if (folder.title === data.current || folder.title === data.selected) { + option.selected = true; + } + + selector.appendChild(option); + }); + + container.style.display = 'block'; + } else { + document.getElementById('navidrome-folder-selector-container').style.display = 'none'; + } + } catch (error) { + console.error('Error loading Navidrome music folders:', error); + document.getElementById('navidrome-folder-selector-container').style.display = 'none'; + } +} + +async function selectNavidromeMusicFolder() { + const selector = document.getElementById('navidrome-music-folder'); + const selectedFolder = selector.value; + + try { + const response = await fetch('/api/navidrome/select-music-folder', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ folder_name: selectedFolder }) + }); + + const data = await response.json(); + + if (data.success) { + showToast(data.message, 'success'); + } else { + console.error('Failed to set music folder:', data.error); + showToast(`Failed to set music folder: ${data.error}`, 'error'); + } + } catch (error) { + console.error('Error selecting Navidrome music folder:', error); + showToast('Error selecting music folder. Please try again.', 'error'); + } +} + // ============================================ // == DISCOVER PAGE == // ============================================