Skip to main content
Glama
library-manager.ts8.76 kB
/** * Navidrome MCP Server - Library Manager Service * Copyright (C) 2025 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ import type { NavidromeClient } from '../client/navidrome-client.js'; import type { Config } from '../config.js'; import { logger } from '../utils/logger.js'; import { ErrorFormatter } from '../utils/error-formatter.js'; interface LibraryInfo { id: number; name: string; path: string; remotePath: string; lastScanAt: string; lastScanStartedAt: string; fullScanInProgress: boolean; updatedAt: string; createdAt: string; totalSongs: number; totalAlbums: number; totalArtists: number; totalFolders: number; totalFiles: number; totalMissingFiles: number; totalSize: number; totalDuration: number; defaultNewUsers: boolean; } interface UserInfo { id: string; userName: string; name: string; email: string; isAdmin: boolean; lastLoginAt: string; lastAccessAt: string; createdAt: string; updatedAt: string; libraries: LibraryInfo[]; } /** * Singleton service for managing library state and filtering across the application */ class LibraryManager { private static instance: LibraryManager | null = null; private userInfo: UserInfo | null = null; private activeLibraryIds: number[] = []; private initialized = false; private constructor() {} /** * Get the singleton instance */ static getInstance(): LibraryManager { LibraryManager.instance ??= new LibraryManager(); return LibraryManager.instance; } /** * Initialize the library manager with user data and default configuration */ async initialize(client: NavidromeClient, config: Config): Promise<void> { if (this.initialized) { logger.debug('LibraryManager already initialized'); return; } try { // Get user info including libraries from authentication await this.loadUserLibraries(client); // Apply default library configuration this.applyDefaultConfiguration(config); this.initialized = true; logger.info(`LibraryManager initialized with ${this.userInfo?.libraries.length ?? 0} libraries, ${this.activeLibraryIds.length} active`); } catch (error) { throw new Error(ErrorFormatter.toolExecution('LibraryManager.initialize', error)); } } /** * Load user libraries from Navidrome API */ private async loadUserLibraries(client: NavidromeClient): Promise<void> { try { // First authenticate to get user ID const token = await (client as unknown as { authManager: { getToken(): Promise<string> } }).authManager.getToken(); // Decode the JWT to get user ID (simple base64 decode) const tokenParts = token.split('.'); if (tokenParts.length !== 3 || tokenParts[1] === null || tokenParts[1] === undefined || tokenParts[1] === '') { throw new Error('Invalid JWT token format'); } const payload = JSON.parse(atob(tokenParts[1])); const userId = payload.uid; if (userId === null || userId === undefined || userId === '') { throw new Error('Unable to extract user ID from token'); } // Get user info including libraries this.userInfo = await client.request<UserInfo>(`/user/${userId}`); logger.debug(`Loaded ${this.userInfo.libraries.length} libraries for user ${this.userInfo.userName}`); } catch (error) { throw new Error(ErrorFormatter.toolExecution('loadUserLibraries', error)); } } /** * Apply default library configuration from config */ private applyDefaultConfiguration(config: Config): void { if (!this.userInfo) { throw new Error('User info not loaded'); } const availableLibraryIds = this.userInfo.libraries.map(lib => lib.id); // Apply default libraries from config if specified if (config.defaultLibraryIds && config.defaultLibraryIds.length > 0) { // Validate that configured library IDs exist const validLibraryIds = config.defaultLibraryIds.filter(id => availableLibraryIds.includes(id) ); if (validLibraryIds.length === 0) { logger.warn(`No valid default libraries found in config. Using all libraries.`); this.activeLibraryIds = availableLibraryIds; } else { this.activeLibraryIds = validLibraryIds; logger.info(`Applied default libraries: ${validLibraryIds.join(', ')}`); } } else { // No default configuration - use all libraries (backward compatibility) this.activeLibraryIds = availableLibraryIds; logger.debug('No default libraries configured, using all libraries'); } } /** * Get all available libraries for the user */ getAvailableLibraries(): LibraryInfo[] { if (!this.userInfo) { throw new Error('LibraryManager not initialized'); } return this.userInfo.libraries; } /** * Get currently active library IDs */ getActiveLibraryIds(): number[] { return [...this.activeLibraryIds]; } /** * Get libraries with active status marked */ getLibrariesWithActiveStatus(): Array<LibraryInfo & { isActive: boolean }> { if (!this.userInfo) { throw new Error('LibraryManager not initialized'); } return this.userInfo.libraries.map(library => ({ ...library, isActive: this.activeLibraryIds.includes(library.id) })); } /** * Set active libraries (replaces current selection) */ setActiveLibraries(libraryIds: number[]): void { if (!this.userInfo) { throw new Error('LibraryManager not initialized'); } const availableLibraryIds = this.userInfo.libraries.map(lib => lib.id); const validLibraryIds = libraryIds.filter(id => availableLibraryIds.includes(id)); if (validLibraryIds.length === 0) { throw new Error(`No valid library IDs provided. Available: ${availableLibraryIds.join(', ')}`); } const invalidIds = libraryIds.filter(id => !availableLibraryIds.includes(id)); if (invalidIds.length > 0) { logger.warn(`Invalid library IDs ignored: ${invalidIds.join(', ')}`); } this.activeLibraryIds = validLibraryIds; logger.info(`Active libraries set to: ${validLibraryIds.join(', ')}`); } /** * Add a library to the active set */ addActiveLibrary(libraryId: number): void { if (!this.userInfo) { throw new Error('LibraryManager not initialized'); } const availableLibraryIds = this.userInfo.libraries.map(lib => lib.id); if (!availableLibraryIds.includes(libraryId)) { throw new Error(`Library ID ${libraryId} not available. Available: ${availableLibraryIds.join(', ')}`); } if (!this.activeLibraryIds.includes(libraryId)) { this.activeLibraryIds.push(libraryId); logger.info(`Added library ${libraryId} to active set`); } } /** * Remove a library from the active set */ removeActiveLibrary(libraryId: number): void { const index = this.activeLibraryIds.indexOf(libraryId); if (index > -1) { this.activeLibraryIds.splice(index, 1); logger.info(`Removed library ${libraryId} from active set`); } } /** * Generate library query parameters for API requests * Returns duplicate parameters in format: library_id=1&library_id=2 */ getLibraryQueryParams(): URLSearchParams { const params = new URLSearchParams(); // Add duplicate library_id parameters as discovered from frontend for (const libraryId of this.activeLibraryIds) { params.append('library_id', libraryId.toString()); } return params; } /** * Get user information */ getUserInfo(): UserInfo | null { return this.userInfo; } /** * Check if library manager is initialized */ isInitialized(): boolean { return this.initialized; } /** * Reset the library manager (for testing) */ reset(): void { this.userInfo = null; this.activeLibraryIds = []; this.initialized = false; LibraryManager.instance = null; } } // Export singleton instance getter for convenience export const libraryManager = LibraryManager.getInstance();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Blakeem/Navidrome-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server