Add music library selection for Navidrome
SoulSync was importing from all Navidrome libraries regardless of user access restrictions. Added a "Music Library" dropdown in Navidrome settings that lets users scope imports to a specific music folder. Uses the Subsonic musicFolderId parameter on artist, album, and search API calls. Selecting "All Libraries" reverts to the previous behavior.
This commit is contained in:
parent
890d47928d
commit
2dc72a39b7
4 changed files with 216 additions and 5 deletions
|
|
@ -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 []
|
||||
|
|
|
|||
|
|
@ -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 ==
|
||||
# ===============================
|
||||
|
|
|
|||
|
|
@ -3012,6 +3012,14 @@
|
|||
<label>Password:</label>
|
||||
<input type="password" id="navidrome-password" placeholder="Password">
|
||||
</div>
|
||||
<div class="form-group" id="navidrome-folder-selector-container"
|
||||
style="display: none;">
|
||||
<label>Music Library</label>
|
||||
<select id="navidrome-music-folder" onchange="selectNavidromeMusicFolder()">
|
||||
<option value="">All Libraries</option>
|
||||
</select>
|
||||
<small>Select which music library to import from</small>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="detect-button"
|
||||
onclick="autoDetectNavidrome()">Auto-detect</button>
|
||||
|
|
|
|||
|
|
@ -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 = '<option value="">All Libraries</option>';
|
||||
|
||||
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 ==
|
||||
// ============================================
|
||||
|
|
|
|||
Loading…
Reference in a new issue