From b42d0252a18ddb18927849fa423fea121335bd99 Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Mon, 8 Dec 2025 10:20:15 -0800 Subject: [PATCH] Add Jellyfin music library selection support Introduces backend and frontend functionality to list and select Jellyfin music libraries. Adds API endpoints, updates the client logic, and provides a UI selector for users to choose their preferred Jellyfin music library. --- core/jellyfin_client.py | 56 ++++++++++++++++++++++++- web_server.py | 52 ++++++++++++++++++++++++ webui/index.html | 8 ++++ webui/static/script.js | 90 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 204 insertions(+), 2 deletions(-) diff --git a/core/jellyfin_client.py b/core/jellyfin_client.py index 6c1db7f0..0e674ab5 100644 --- a/core/jellyfin_client.py +++ b/core/jellyfin_client.py @@ -266,7 +266,61 @@ class JellyfinClient: except Exception as e: logger.error(f"Error finding music library: {e}") - + + def get_available_music_libraries(self) -> List[Dict[str, str]]: + """Get list of all available music libraries from Jellyfin""" + if not self.ensure_connection() or not self.user_id: + return [] + + try: + views_response = self._make_request(f'/Users/{self.user_id}/Views') + if not views_response: + return [] + + music_libraries = [] + for view in views_response.get('Items', []): + collection_type = (view.get('CollectionType') or '').lower() + if collection_type == 'music': + music_libraries.append({ + 'title': view.get('Name', 'Music'), + 'key': str(view['Id']) + }) + + logger.debug(f"Found {len(music_libraries)} music libraries") + return music_libraries + except Exception as e: + logger.error(f"Error getting music libraries: {e}") + return [] + + def set_music_library_by_name(self, library_name: str) -> bool: + """Set the active music library by name""" + if not self.user_id: + return False + + try: + views_response = self._make_request(f'/Users/{self.user_id}/Views') + if not views_response: + return False + + for view in views_response.get('Items', []): + collection_type = (view.get('CollectionType') or '').lower() + if collection_type == 'music' and view.get('Name') == library_name: + self.music_library_id = view['Id'] + logger.info(f"Set music library to: {library_name}") + + # Store preference in database + from database.music_database import MusicDatabase + db = MusicDatabase() + db.set_preference('jellyfin_music_library', library_name) + + return True + + logger.warning(f"Music library '{library_name}' not found") + return False + except Exception as e: + logger.error(f"Error setting music library: {e}") + return False + def _make_request(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]: """Make authenticated request to Jellyfin API""" if not self.base_url or not self.api_key: diff --git a/web_server.py b/web_server.py index fe558135..00796da7 100644 --- a/web_server.py +++ b/web_server.py @@ -2244,6 +2244,58 @@ def select_plex_music_library(): logger.error(f"Error setting Plex music library: {e}") return jsonify({"success": False, "error": str(e)}), 500 +@app.route('/api/jellyfin/music-libraries', methods=['GET']) +def get_jellyfin_music_libraries(): + """Get list of all available music libraries from Jellyfin""" + try: + libraries = jellyfin_client.get_available_music_libraries() + + # Get currently selected library + from database.music_database import MusicDatabase + db = MusicDatabase() + selected_library = db.get_preference('jellyfin_music_library') + + # Get the currently active library name (match Plex behavior) + current_library = None + if jellyfin_client.music_library_id: + # Look up library name from ID + for lib in libraries: + if lib['key'] == jellyfin_client.music_library_id: + current_library = lib['title'] + break + + return jsonify({ + "success": True, + "libraries": libraries, + "selected": selected_library, + "current": current_library + }) + except Exception as e: + logger.error(f"Error getting Jellyfin music libraries: {e}") + return jsonify({"success": False, "error": str(e)}), 500 + +@app.route('/api/jellyfin/select-music-library', methods=['POST']) +def select_jellyfin_music_library(): + """Set the active Jellyfin music library""" + try: + data = request.get_json() + library_name = data.get('library_name') + + if not library_name: + return jsonify({"success": False, "error": "No library name provided"}), 400 + + success = jellyfin_client.set_music_library_by_name(library_name) + + if success: + add_activity_item("📚", "Library Selected", f"Jellyfin music library set to: {library_name}", "Now") + return jsonify({"success": True, "message": f"Music library set to: {library_name}"}) + else: + return jsonify({"success": False, "error": f"Library '{library_name}' not found"}), 404 + + except Exception as e: + logger.error(f"Error setting Jellyfin music library: {e}") + return jsonify({"success": False, "error": str(e)}), 500 + # =============================== # == QUALITY PROFILE API == # =============================== diff --git a/webui/index.html b/webui/index.html index 80dd5cba..1ffe7a6c 100644 --- a/webui/index.html +++ b/webui/index.html @@ -2353,6 +2353,14 @@ + + diff --git a/webui/static/script.js b/webui/static/script.js index 048b56e7..8f4b9fd8 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -1504,6 +1504,11 @@ async function loadSettingsData() { loadPlexMusicLibraries(); } + // Load Jellyfin music libraries if Jellyfin is the active server + if (activeServer === 'jellyfin') { + loadJellyfinMusicLibraries(); + } + // Populate Soulseek settings document.getElementById('soulseek-url').value = settings.soulseek?.slskd_url || ''; document.getElementById('soulseek-api-key').value = settings.soulseek?.api_key || ''; @@ -1576,6 +1581,11 @@ function toggleServer(serverType) { if (serverType === 'plex') { loadPlexMusicLibraries(); } + + // Load Jellyfin music libraries when switching to Jellyfin + if (serverType === 'jellyfin') { + loadJellyfinMusicLibraries(); + } } // =============================== @@ -1908,9 +1918,16 @@ async function testConnection(service) { }); const result = await response.json(); - + if (result.success) { showToast(`${service} connection successful`, 'success'); + + // Load music libraries after successful connection + if (service === 'plex') { + loadPlexMusicLibraries(); + } else if (service === 'jellyfin') { + loadJellyfinMusicLibraries(); + } } else { showToast(`${service} connection failed: ${result.error}`, 'error'); } @@ -26667,6 +26684,77 @@ async function selectPlexLibrary() { } } +// ============ Jellyfin Music Library Selection ============ + +async function loadJellyfinMusicLibraries() { + try { + const response = await fetch('/api/jellyfin/music-libraries'); + const data = await response.json(); + + if (data.success && data.libraries && data.libraries.length > 0) { + const selector = document.getElementById('jellyfin-music-library'); + const container = document.getElementById('jellyfin-library-selector-container'); + + // Clear existing options + selector.innerHTML = ''; + + // Add options for each library + data.libraries.forEach(library => { + const option = document.createElement('option'); + option.value = library.title; + option.textContent = library.title; + + // Mark the currently selected library + if (library.title === data.current || library.title === data.selected) { + option.selected = true; + } + + selector.appendChild(option); + }); + + // Show the container + container.style.display = 'block'; + } else { + // Hide if no libraries found or not connected + document.getElementById('jellyfin-library-selector-container').style.display = 'none'; + } + } catch (error) { + console.error('Error loading Jellyfin music libraries:', error); + document.getElementById('jellyfin-library-selector-container').style.display = 'none'; + } +} + +async function selectJellyfinLibrary() { + const selector = document.getElementById('jellyfin-music-library'); + const selectedLibrary = selector.value; + + if (!selectedLibrary) return; + + try { + const response = await fetch('/api/jellyfin/select-music-library', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + library_name: selectedLibrary + }) + }); + + const data = await response.json(); + + if (data.success) { + console.log(`Jellyfin music library switched to: ${selectedLibrary}`); + } else { + console.error('Failed to switch library:', data.error); + alert(`Failed to switch library: ${data.error}`); + } + } catch (error) { + console.error('Error selecting Jellyfin library:', error); + alert('Error selecting library. Please try again.'); + } +} + // ============================================ // == DISCOVER PAGE == // ============================================