diff --git a/app/frontend/__tests__/lyrics.test.js b/app/frontend/__tests__/lyrics.test.js index 39f5643c..0eb99296 100644 --- a/app/frontend/__tests__/lyrics.test.js +++ b/app/frontend/__tests__/lyrics.test.js @@ -98,7 +98,7 @@ describe('lyrics API', () => { await lyrics.clearCache(); - expect(mockInvoke).toHaveBeenCalledWith('lyrics_clear_cache'); + expect(mockInvoke).toHaveBeenCalledWith('lyrics_clear_cache', {}); }); }); }); diff --git a/app/frontend/js/api/agent.js b/app/frontend/js/api/agent.js index 175c5202..4976f348 100644 --- a/app/frontend/js/api/agent.js +++ b/app/frontend/js/api/agent.js @@ -2,10 +2,10 @@ * Agent API * * Conversational playlist generation via local LLM (Ollama + Rig). - * All commands return graceful fallbacks when the agent feature is disabled. + * All operations return graceful fallbacks when not running in Tauri. */ -import { ApiError, invoke } from './shared.js'; +import { tauriInvoke } from './shared.js'; export const agent = { /** @@ -14,15 +14,9 @@ export const agent = { * @returns {Promise<{status: string, playlist_id?: number, playlist_name?: string, track_count?: number, message: string}>} */ async generatePlaylist(prompt) { - if (invoke) { - try { - return await invoke('agent_generate_playlist', { prompt }); - } catch (error) { - console.error('[api.agent.generatePlaylist] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } - throw new ApiError(501, 'Agent requires Tauri runtime'); + const result = await tauriInvoke('agent_generate_playlist', { prompt }); + if (result !== null) return result; + throw new Error('Agent requires Tauri runtime'); }, /** @@ -30,14 +24,8 @@ export const agent = { * @returns {Promise<{available: boolean, model: string, message: string}>} */ async checkStatus() { - if (invoke) { - try { - return await invoke('agent_check_status'); - } catch (error) { - console.error('[api.agent.checkStatus] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('agent_check_status'); + if (result !== null) return result; return { available: false, model: '', message: 'Agent requires Tauri runtime' }; }, @@ -46,14 +34,8 @@ export const agent = { * @returns {Promise<{connected: boolean, models: string[]}>} */ async checkOllama() { - if (invoke) { - try { - return await invoke('agent_check_ollama'); - } catch (error) { - console.error('[api.agent.checkOllama] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('agent_check_ollama'); + if (result !== null) return result; return { connected: false, models: [] }; }, @@ -63,15 +45,9 @@ export const agent = { * @returns {Promise<{success: boolean, model: string, message: string}>} */ async pullModel(model) { - if (invoke) { - try { - return await invoke('agent_pull_model', { model }); - } catch (error) { - console.error('[api.agent.pullModel] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } - throw new ApiError(501, 'Agent requires Tauri runtime'); + const result = await tauriInvoke('agent_pull_model', { model }); + if (result !== null) return result; + throw new Error('Agent requires Tauri runtime'); }, /** @@ -79,14 +55,8 @@ export const agent = { * @returns {Promise<{completed: boolean, model?: string}>} */ async getOnboardingState() { - if (invoke) { - try { - return await invoke('agent_get_onboarding_state'); - } catch (error) { - console.error('[api.agent.getOnboardingState] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('agent_get_onboarding_state'); + if (result !== null) return result; return { completed: false, model: null }; }, @@ -95,14 +65,7 @@ export const agent = { * @param {string|null} model - Model name used * @returns {Promise} */ - async setOnboardingComplete(model = null) { - if (invoke) { - try { - return await invoke('agent_set_onboarding_complete', { model }); - } catch (error) { - console.error('[api.agent.setOnboardingComplete] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + setOnboardingComplete(model = null) { + return tauriInvoke('agent_set_onboarding_complete', { model }); }, }; diff --git a/app/frontend/js/api/audio.js b/app/frontend/js/api/audio.js index d5bc303a..62f292a6 100644 --- a/app/frontend/js/api/audio.js +++ b/app/frontend/js/api/audio.js @@ -4,7 +4,7 @@ * Audio output device enumeration and selection via Tauri commands. */ -import { ApiError, invoke } from './shared.js'; +import { tauriInvoke } from './shared.js'; export const audio = { /** @@ -12,15 +12,8 @@ export const audio = { * @returns {Promise<{devices: string[]}>} */ async listDevices() { - if (invoke) { - try { - return await invoke('audio_list_devices'); - } catch (error) { - console.error('[api.audio.listDevices] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } - // No HTTP fallback — audio device selection requires Tauri runtime + const result = await tauriInvoke('audio_list_devices'); + if (result !== null) return result; return { devices: [] }; }, @@ -29,14 +22,7 @@ export const audio = { * @param {string|null} deviceName - Device name, or null for system default * @returns {Promise} */ - async setDevice(deviceName) { - if (invoke) { - try { - return await invoke('audio_set_device', { deviceName }); - } catch (error) { - console.error('[api.audio.setDevice] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + setDevice(deviceName) { + return tauriInvoke('audio_set_device', { deviceName }); }, }; diff --git a/app/frontend/js/api/favorites.js b/app/frontend/js/api/favorites.js index d7f72473..dfe674fd 100644 --- a/app/frontend/js/api/favorites.js +++ b/app/frontend/js/api/favorites.js @@ -4,7 +4,7 @@ * Liked songs, top played, recently played/added operations. */ -import { ApiError, invoke, request } from './shared.js'; +import { ApiError, request, tauriInvoke } from './shared.js'; export const favorites = { /** @@ -15,17 +15,11 @@ export const favorites = { * @returns {Promise<{tracks: Array, total: number, limit: number, offset: number}>} */ async get(params = {}) { - if (invoke) { - try { - return await invoke('favorites_get', { - limit: params.limit ?? null, - offset: params.offset ?? null, - }); - } catch (error) { - console.error('[api.favorites.get] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('favorites_get', { + limit: params.limit ?? null, + offset: params.offset ?? null, + }); + if (result !== null) return result; // Fallback to HTTP const query = new URLSearchParams(); if (params.limit) query.set('limit', params.limit.toString()); @@ -40,14 +34,8 @@ export const favorites = { * @returns {Promise<{is_favorite: boolean, favorited_date: string|null}>} */ async check(trackId) { - if (invoke) { - try { - return await invoke('favorites_check', { trackId }); - } catch (error) { - console.error('[api.favorites.check] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('favorites_check', { trackId }); + if (result !== null) return result; return request(`/favorites/${encodeURIComponent(trackId)}`); }, @@ -57,20 +45,17 @@ export const favorites = { * @returns {Promise<{success: boolean, favorited_date: string}>} */ async add(trackId) { - if (invoke) { - try { - return await invoke('favorites_add', { trackId }); - } catch (error) { - console.error('[api.favorites.add] Tauri error:', error); - // Check for specific error messages - if (error.toString().includes('already favorited')) { - throw new ApiError(409, 'Track is already favorited'); - } - if (error.toString().includes('not found')) { - throw new ApiError(404, error.toString()); - } - throw new ApiError(500, error.toString()); + try { + const result = await tauriInvoke('favorites_add', { trackId }); + if (result !== null) return result; + } catch (error) { + if (error.message.includes('already favorited')) { + throw new ApiError(409, 'Track is already favorited'); + } + if (error.message.includes('not found')) { + throw new ApiError(404, error.message); } + throw error; } return request(`/favorites/${encodeURIComponent(trackId)}`, { method: 'POST', @@ -83,16 +68,14 @@ export const favorites = { * @returns {Promise} */ async remove(trackId) { - if (invoke) { - try { - return await invoke('favorites_remove', { trackId }); - } catch (error) { - console.error('[api.favorites.remove] Tauri error:', error); - if (error.toString().includes('not in favorites')) { - throw new ApiError(404, error.toString()); - } - throw new ApiError(500, error.toString()); + try { + const result = await tauriInvoke('favorites_remove', { trackId }); + if (result !== null) return result; + } catch (error) { + if (error.message.includes('not in favorites')) { + throw new ApiError(404, error.message); } + throw error; } return request(`/favorites/${encodeURIComponent(trackId)}`, { method: 'DELETE', @@ -104,14 +87,8 @@ export const favorites = { * @returns {Promise<{tracks: Array}>} */ async getTop25() { - if (invoke) { - try { - return await invoke('favorites_get_top25'); - } catch (error) { - console.error('[api.favorites.getTop25] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('favorites_get_top25'); + if (result !== null) return result; return request('/favorites/top25'); }, @@ -123,17 +100,11 @@ export const favorites = { * @returns {Promise<{tracks: Array, days: number}>} */ async getRecentlyPlayed(params = {}) { - if (invoke) { - try { - return await invoke('favorites_get_recently_played', { - days: params.days ?? null, - limit: params.limit ?? null, - }); - } catch (error) { - console.error('[api.favorites.getRecentlyPlayed] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('favorites_get_recently_played', { + days: params.days ?? null, + limit: params.limit ?? null, + }); + if (result !== null) return result; // Fallback to HTTP const query = new URLSearchParams(); if (params.days) query.set('days', params.days.toString()); @@ -150,17 +121,11 @@ export const favorites = { * @returns {Promise<{tracks: Array, days: number}>} */ async getRecentlyAdded(params = {}) { - if (invoke) { - try { - return await invoke('favorites_get_recently_added', { - days: params.days ?? null, - limit: params.limit ?? null, - }); - } catch (error) { - console.error('[api.favorites.getRecentlyAdded] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('favorites_get_recently_added', { + days: params.days ?? null, + limit: params.limit ?? null, + }); + if (result !== null) return result; // Fallback to HTTP const query = new URLSearchParams(); if (params.days) query.set('days', params.days.toString()); diff --git a/app/frontend/js/api/lastfm.js b/app/frontend/js/api/lastfm.js index c0cf9530..7dcc953a 100644 --- a/app/frontend/js/api/lastfm.js +++ b/app/frontend/js/api/lastfm.js @@ -4,7 +4,7 @@ * Scrobbling, authentication, loved tracks, and now-playing operations. */ -import { ApiError, invoke, request } from './shared.js'; +import { request, tauriInvoke } from './shared.js'; export const lastfm = { /** @@ -12,14 +12,8 @@ export const lastfm = { * @returns {Promise<{enabled: boolean, username: string|null, authenticated: boolean, configured: boolean, scrobble_threshold: number}>} */ async getSettings() { - if (invoke) { - try { - return await invoke('lastfm_get_settings'); - } catch (error) { - console.error('[api.lastfm.getSettings] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('lastfm_get_settings'); + if (result !== null) return result; return request('/lastfm/settings'); }, @@ -31,14 +25,8 @@ export const lastfm = { * @returns {Promise<{updated: string[]}>} */ async updateSettings(settings) { - if (invoke) { - try { - return await invoke('lastfm_update_settings', { settingsUpdate: settings }); - } catch (error) { - console.error('[api.lastfm.updateSettings] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('lastfm_update_settings', { settingsUpdate: settings }); + if (result !== null) return result; return request('/lastfm/settings', { method: 'PUT', body: JSON.stringify(settings), @@ -50,14 +38,8 @@ export const lastfm = { * @returns {Promise<{auth_url: string, token: string}>} */ async getAuthUrl() { - if (invoke) { - try { - return await invoke('lastfm_get_auth_url'); - } catch (error) { - console.error('[api.lastfm.getAuthUrl] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('lastfm_get_auth_url'); + if (result !== null) return result; return request('/lastfm/auth-url'); }, @@ -67,14 +49,8 @@ export const lastfm = { * @returns {Promise<{status: string, username: string, message: string}>} */ async completeAuth(token) { - if (invoke) { - try { - return await invoke('lastfm_auth_callback', { token }); - } catch (error) { - console.error('[api.lastfm.completeAuth] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('lastfm_auth_callback', { token }); + if (result !== null) return result; const query = new URLSearchParams({ token }); return request(`/lastfm/auth-callback?${query}`); }, @@ -91,14 +67,8 @@ export const lastfm = { * @returns {Promise<{status: string, message?: string}>} */ async scrobble(scrobbleData) { - if (invoke) { - try { - return await invoke('lastfm_scrobble', { request: scrobbleData }); - } catch (error) { - console.error('[api.lastfm.scrobble] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('lastfm_scrobble', { request: scrobbleData }); + if (result !== null) return result; return request('/lastfm/scrobble', { method: 'POST', body: JSON.stringify(scrobbleData), @@ -115,14 +85,8 @@ export const lastfm = { * @returns {Promise<{status: string, message?: string}>} */ async updateNowPlaying(nowPlayingData) { - if (invoke) { - try { - return await invoke('lastfm_now_playing', { request: nowPlayingData }); - } catch (error) { - console.error('[api.lastfm.updateNowPlaying] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('lastfm_now_playing', { request: nowPlayingData }); + if (result !== null) return result; return request('/lastfm/now-playing', { method: 'POST', body: JSON.stringify(nowPlayingData), @@ -134,14 +98,8 @@ export const lastfm = { * @returns {Promise<{status: string, total_loved_tracks: number, imported_count: number, message: string}>} */ async importLovedTracks() { - if (invoke) { - try { - return await invoke('lastfm_import_loved_tracks'); - } catch (error) { - console.error('[api.lastfm.importLovedTracks] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('lastfm_import_loved_tracks'); + if (result !== null) return result; return request('/lastfm/import-loved-tracks', { method: 'POST', }); @@ -152,14 +110,8 @@ export const lastfm = { * @returns {Promise<{status: string, message: string}>} */ async disconnect() { - if (invoke) { - try { - return await invoke('lastfm_disconnect'); - } catch (error) { - console.error('[api.lastfm.disconnect] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('lastfm_disconnect'); + if (result !== null) return result; return request('/lastfm/disconnect', { method: 'DELETE', }); @@ -170,14 +122,8 @@ export const lastfm = { * @returns {Promise<{queued_scrobbles: number}>} */ async getQueueStatus() { - if (invoke) { - try { - return await invoke('lastfm_queue_status'); - } catch (error) { - console.error('[api.lastfm.getQueueStatus] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('lastfm_queue_status'); + if (result !== null) return result; return request('/lastfm/queue/status'); }, @@ -186,14 +132,8 @@ export const lastfm = { * @returns {Promise<{status: string, remaining_queued: number}>} */ async retryQueuedScrobbles() { - if (invoke) { - try { - return await invoke('lastfm_queue_retry'); - } catch (error) { - console.error('[api.lastfm.retryQueuedScrobbles] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('lastfm_queue_retry'); + if (result !== null) return result; return request('/lastfm/queue/retry', { method: 'POST', }); @@ -204,14 +144,8 @@ export const lastfm = { * @returns {Promise<{status: string, fetched: number, new_tracks: number, total_cached: number}>} */ async cacheLovedTracks() { - if (invoke) { - try { - return await invoke('lastfm_cache_loved_tracks'); - } catch (error) { - console.error('[api.lastfm.cacheLovedTracks] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('lastfm_cache_loved_tracks'); + if (result !== null) return result; return request('/lastfm/cache-loved-tracks', { method: 'POST', }); @@ -222,14 +156,8 @@ export const lastfm = { * @returns {Promise<{status: string, matched: number, already_matched: number, not_found: number, new_favorites: number}>} */ async matchLovedTracks() { - if (invoke) { - try { - return await invoke('lastfm_match_loved_tracks'); - } catch (error) { - console.error('[api.lastfm.matchLovedTracks] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('lastfm_match_loved_tracks'); + if (result !== null) return result; return request('/lastfm/match-loved-tracks', { method: 'POST', }); @@ -240,14 +168,8 @@ export const lastfm = { * @returns {Promise<{total_cached: number, matched: number, unmatched: number, most_recent_loved: number|null}>} */ async getLovedStats() { - if (invoke) { - try { - return await invoke('lastfm_loved_stats'); - } catch (error) { - console.error('[api.lastfm.getLovedStats] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('lastfm_loved_stats'); + if (result !== null) return result; return request('/lastfm/loved-stats'); }, @@ -256,14 +178,8 @@ export const lastfm = { * @returns {Promise<{status: string, cleared: number, message: string}>} */ async resetLovedCache() { - if (invoke) { - try { - return await invoke('lastfm_reset_loved_cache'); - } catch (error) { - console.error('[api.lastfm.resetLovedCache] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('lastfm_reset_loved_cache'); + if (result !== null) return result; return request('/lastfm/reset-loved-cache', { method: 'POST' }); }, }; diff --git a/app/frontend/js/api/library.js b/app/frontend/js/api/library.js index d60421aa..177b8bf2 100644 --- a/app/frontend/js/api/library.js +++ b/app/frontend/js/api/library.js @@ -4,7 +4,7 @@ * Track management, scanning, artwork, and missing track operations. */ -import { ApiError, invoke, request } from './shared.js'; +import { ApiError, request, tauriInvoke } from './shared.js'; export const library = { /** @@ -16,18 +16,12 @@ export const library = { * @returns {Promise<{total: number, total_duration: number}>} */ async getCount(params = {}) { - if (invoke) { - try { - return await invoke('library_get_count', { - search: params.search || null, - artist: params.artist || null, - album: params.album || null, - }); - } catch (error) { - console.error('[api.library.getCount] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('library_get_count', { + search: params.search || null, + artist: params.artist || null, + album: params.album || null, + }); + if (result !== null) return result; const query = new URLSearchParams(); if (params.search) query.set('search', params.search); const queryString = query.toString(); @@ -45,22 +39,16 @@ export const library = { * @returns {Promise} 0-based offset or null */ async findOffset(params = {}) { - if (invoke) { - try { - return await invoke('library_find_offset', { - search: params.search || null, - artist: params.artist || null, - album: params.album || null, - sortBy: params.sort || null, - sortOrder: params.order || null, - ignoreWords: params.ignoreWords || null, - prefix: params.prefix, - }); - } catch (error) { - console.error('[api.library.findOffset] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('library_find_offset', { + search: params.search || null, + artist: params.artist || null, + album: params.album || null, + sortBy: params.sort || null, + sortOrder: params.order || null, + ignoreWords: params.ignoreWords || null, + prefix: params.prefix, + }); + if (result !== null) return result; return null; }, @@ -75,23 +63,17 @@ export const library = { * @returns {Promise<{tracks: Array, total: number, limit: number, offset: number}>} */ async getTracks(params = {}) { - if (invoke) { - try { - return await invoke('library_get_all', { - search: params.search || null, - artist: params.artist || null, - album: params.album || null, - sortBy: params.sort || null, - sortOrder: params.order || null, - limit: params.limit || null, - offset: params.offset || null, - ignoreWords: params.ignoreWords || null, - }); - } catch (error) { - console.error('[api.library.getTracks] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('library_get_all', { + search: params.search || null, + artist: params.artist || null, + album: params.album || null, + sortBy: params.sort || null, + sortOrder: params.order || null, + limit: params.limit || null, + offset: params.offset || null, + ignoreWords: params.ignoreWords || null, + }); + if (result !== null) return result; // Fallback to HTTP const query = new URLSearchParams(); if (params.search) query.set('search', params.search); @@ -121,25 +103,19 @@ export const library = { * @returns {Promise<{section: string, tracks: Array, total_tracks: number, total_duration: number, page: number|null, page_size: number|null, has_more: boolean, revision: number}>} */ async getSection(params = {}) { - if (invoke) { - try { - return await invoke('library_get_section', { - section: params.section, - search: params.search || null, - artist: params.artist || null, - album: params.album || null, - sortBy: params.sort || null, - sortOrder: params.order || null, - limit: params.limit || null, - offset: params.offset || null, - ignoreWords: params.ignoreWords || null, - days: params.days || null, - }); - } catch (error) { - console.error('[api.library.getSection] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('library_get_section', { + section: params.section, + search: params.search || null, + artist: params.artist || null, + album: params.album || null, + sortBy: params.sort || null, + sortOrder: params.order || null, + limit: params.limit || null, + offset: params.offset || null, + ignoreWords: params.ignoreWords || null, + days: params.days || null, + }); + if (result !== null) return result; // HTTP fallback: dispatch to the appropriate REST endpoint per section const section = params.section || 'all'; @@ -193,14 +169,8 @@ export const library = { * @returns {Promise} Track object or null */ async getTrack(id) { - if (invoke) { - try { - return await invoke('library_get_track', { trackId: id }); - } catch (error) { - console.error('[api.library.getTrack] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('library_get_track', { trackId: id }); + if (result !== null) return result; return request(`/library/${encodeURIComponent(id)}`); }, @@ -211,20 +181,15 @@ export const library = { * @returns {Promise<{added_count: number, modified_count: number, unchanged_count: number, deleted_count: number, error_count: number}>} */ async scan(paths, recursive = true) { - if (invoke) { - try { - const result = await invoke('scan_paths_to_library', { paths, recursive }); - // Map response to expected format - return { - added: result.added_count || 0, - skipped: result.unchanged_count || 0, - errors: result.error_count || 0, - tracks: [], // The new API doesn't return tracks - }; - } catch (error) { - console.error('[api.library.scan] Tauri error:', error); - throw new ApiError(500, error.toString()); - } + const result = await tauriInvoke('scan_paths_to_library', { paths, recursive }); + if (result !== null) { + // Map response to expected format + return { + added: result.added_count || 0, + skipped: result.unchanged_count || 0, + errors: result.error_count || 0, + tracks: [], // The new API doesn't return tracks + }; } return request('/library/scan', { method: 'POST', @@ -237,14 +202,8 @@ export const library = { * @returns {Promise<{total_tracks: number, total_duration: number, total_size: number, total_artists: number, total_albums: number}>} */ async getStats() { - if (invoke) { - try { - return await invoke('library_get_stats'); - } catch (error) { - console.error('[api.library.getStats] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('library_get_stats'); + if (result !== null) return result; return request('/library/stats'); }, @@ -254,14 +213,8 @@ export const library = { * @returns {Promise} True if deleted */ async deleteTrack(id) { - if (invoke) { - try { - return await invoke('library_delete_track', { trackId: id }); - } catch (error) { - console.error('[api.library.deleteTrack] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('library_delete_track', { trackId: id }); + if (result !== null) return result; return request(`/library/${encodeURIComponent(id)}`, { method: 'DELETE', }); @@ -273,14 +226,8 @@ export const library = { * @returns {Promise} Updated track object */ async updatePlayCount(id) { - if (invoke) { - try { - return await invoke('library_update_play_count', { trackId: id }); - } catch (error) { - console.error('[api.library.updatePlayCount] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('library_update_play_count', { trackId: id }); + if (result !== null) return result; return request(`/library/${encodeURIComponent(id)}/play-count`, { method: 'PUT', }); @@ -292,14 +239,8 @@ export const library = { * @returns {Promise} Updated track object */ async rescanTrack(id) { - if (invoke) { - try { - return await invoke('library_rescan_track', { trackId: id }); - } catch (error) { - console.error('[api.library.rescanTrack] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('library_rescan_track', { trackId: id }); + if (result !== null) return result; return request(`/library/${encodeURIComponent(id)}/rescan`, { method: 'PUT', }); @@ -311,17 +252,14 @@ export const library = { * @returns {Promise<{data: string, mime_type: string, source: string}|null>} */ async getArtwork(id) { - if (invoke) { - try { - return await invoke('library_get_artwork', { trackId: id }); - } catch (error) { - // Not found is returned as null, not an error - if (error.toString().includes('not found')) { - return null; - } - console.error('[api.library.getArtwork] Tauri error:', error); - throw new ApiError(500, error.toString()); + try { + const result = await tauriInvoke('library_get_artwork', { trackId: id }); + if (result !== null) return result; + } catch (error) { + if (error.message.includes('not found')) { + return null; } + throw error; } try { return await request(`/library/${encodeURIComponent(id)}/artwork`); @@ -339,9 +277,9 @@ export const library = { * @returns {Promise} Data URL or null */ async getArtworkUrl(id) { - if (invoke) { + if (window.__TAURI__?.core?.invoke) { try { - return await invoke('library_get_artwork_url', { trackId: id }); + return await window.__TAURI__.core.invoke('library_get_artwork_url', { trackId: id }); } catch (error) { if (error.toString().includes('not found')) { return null; @@ -363,14 +301,8 @@ export const library = { * @returns {Promise<{tracks: Array, total: number}>} */ async getMissing() { - if (invoke) { - try { - return await invoke('library_get_missing'); - } catch (error) { - console.error('[api.library.getMissing] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('library_get_missing'); + if (result !== null) return result; return request('/library/missing'); }, @@ -381,14 +313,8 @@ export const library = { * @returns {Promise} Updated track object */ async locate(id, newPath) { - if (invoke) { - try { - return await invoke('library_locate_track', { trackId: id, newPath }); - } catch (error) { - console.error('[api.library.locate] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('library_locate_track', { trackId: id, newPath }); + if (result !== null) return result; return request(`/library/${encodeURIComponent(id)}/locate`, { method: 'POST', body: JSON.stringify({ new_path: newPath }), @@ -401,14 +327,8 @@ export const library = { * @returns {Promise} Updated track object with current missing status */ async checkStatus(id) { - if (invoke) { - try { - return await invoke('library_check_status', { trackId: id }); - } catch (error) { - console.error('[api.library.checkStatus] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('library_check_status', { trackId: id }); + if (result !== null) return result; return request(`/library/${encodeURIComponent(id)}/check-status`, { method: 'POST', }); @@ -420,14 +340,8 @@ export const library = { * @returns {Promise} Updated track object */ async markMissing(id) { - if (invoke) { - try { - return await invoke('library_mark_missing', { trackId: id }); - } catch (error) { - console.error('[api.library.markMissing] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('library_mark_missing', { trackId: id }); + if (result !== null) return result; return request(`/library/${encodeURIComponent(id)}/mark-missing`, { method: 'POST', }); @@ -439,14 +353,8 @@ export const library = { * @returns {Promise} Updated track object */ async markPresent(id) { - if (invoke) { - try { - return await invoke('library_mark_present', { trackId: id }); - } catch (error) { - console.error('[api.library.markPresent] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('library_mark_present', { trackId: id }); + if (result !== null) return result; return request(`/library/${encodeURIComponent(id)}/mark-present`, { method: 'POST', }); diff --git a/app/frontend/js/api/lyrics.js b/app/frontend/js/api/lyrics.js index 6ec01989..517c33de 100644 --- a/app/frontend/js/api/lyrics.js +++ b/app/frontend/js/api/lyrics.js @@ -4,7 +4,7 @@ * LRCLIB lyrics lookup with SQLite caching via Tauri commands. */ -import { ApiError, invoke } from './shared.js'; +import { tauriInvoke } from './shared.js'; export const lyrics = { /** @@ -17,19 +17,13 @@ export const lyrics = { * @returns {Promise<{plain_lyrics: string|null, synced_lyrics: string|null, instrumental: boolean}|null>} */ async get(params) { - if (invoke) { - try { - return await invoke('lyrics_get', { - artist: params.artist, - title: params.title, - album: params.album ?? null, - duration: params.duration ?? null, - }); - } catch (error) { - console.error('[api.lyrics.get] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('lyrics_get', { + artist: params.artist, + title: params.title, + album: params.album ?? null, + duration: params.duration ?? null, + }); + if (result !== null) return result; return null; }, @@ -37,14 +31,7 @@ export const lyrics = { * Clear all cached lyrics * @returns {Promise} */ - async clearCache() { - if (invoke) { - try { - return await invoke('lyrics_clear_cache'); - } catch (error) { - console.error('[api.lyrics.clearCache] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + clearCache() { + return tauriInvoke('lyrics_clear_cache'); }, }; diff --git a/app/frontend/js/api/playlists.js b/app/frontend/js/api/playlists.js index be17a803..50c9c603 100644 --- a/app/frontend/js/api/playlists.js +++ b/app/frontend/js/api/playlists.js @@ -1,10 +1,10 @@ /** - * Playlists API + * PlaylistsAPI * * Playlist CRUD, track management, and reordering operations. */ -import { ApiError, invoke, request } from './shared.js'; +import { request, tauriInvoke } from './shared.js'; export const playlists = { /** @@ -12,15 +12,8 @@ export const playlists = { * @returns {Promise} Array of playlists */ async getAll() { - if (invoke) { - try { - const response = await invoke('playlist_list'); - return response.playlists || []; - } catch (error) { - console.error('[api.playlists.getAll] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('playlist_list'); + if (result !== null) return result.playlists || []; const response = await request('/playlists'); return Array.isArray(response) ? response : (response.playlists || []); }, @@ -31,14 +24,8 @@ export const playlists = { * @returns {Promise<{name: string}>} */ async generateName(base = 'New playlist') { - if (invoke) { - try { - return await invoke('playlist_generate_name', { base }); - } catch (error) { - console.error('[api.playlists.generateName] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('playlist_generate_name', { base }); + if (result !== null) return result; const query = new URLSearchParams({ base }); return request(`/playlists/generate-name?${query}`); }, @@ -49,15 +36,8 @@ export const playlists = { * @returns {Promise<{playlist: object|null}>} */ async create(name) { - if (invoke) { - try { - const response = await invoke('playlist_create', { name }); - return response.playlist; - } catch (error) { - console.error('[api.playlists.create] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('playlist_create', { name }); + if (result !== null) return result.playlist; return request('/playlists', { method: 'POST', body: JSON.stringify({ name }), @@ -70,14 +50,8 @@ export const playlists = { * @returns {Promise} */ async get(playlistId) { - if (invoke) { - try { - return await invoke('playlist_get', { playlistId }); - } catch (error) { - console.error('[api.playlists.get] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('playlist_get', { playlistId }); + if (result !== null) return result; return request(`/playlists/${playlistId}`); }, @@ -88,14 +62,8 @@ export const playlists = { * @returns {Promise<{playlist: object|null}>} */ async rename(playlistId, name) { - if (invoke) { - try { - return await invoke('playlist_update', { playlistId, name }); - } catch (error) { - console.error('[api.playlists.rename] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('playlist_update', { playlistId, name }); + if (result !== null) return result; return request(`/playlists/${playlistId}`, { method: 'PUT', body: JSON.stringify({ name }), @@ -108,14 +76,8 @@ export const playlists = { * @returns {Promise<{success: boolean}>} */ async delete(playlistId) { - if (invoke) { - try { - return await invoke('playlist_delete', { playlistId }); - } catch (error) { - console.error('[api.playlists.delete] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('playlist_delete', { playlistId }); + if (result !== null) return result; return request(`/playlists/${playlistId}`, { method: 'DELETE', }); @@ -129,18 +91,12 @@ export const playlists = { * @returns {Promise<{added: number, track_count: number}>} */ async addTracks(playlistId, trackIds, position) { - if (invoke) { - try { - return await invoke('playlist_add_tracks', { - playlistId, - trackIds, - position: position ?? null, - }); - } catch (error) { - console.error('[api.playlists.addTracks] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('playlist_add_tracks', { + playlistId, + trackIds, + position: position ?? null, + }); + if (result !== null) return result; return request(`/playlists/${playlistId}/tracks`, { method: 'POST', body: JSON.stringify({ track_ids: trackIds }), @@ -154,14 +110,8 @@ export const playlists = { * @returns {Promise<{success: boolean}>} */ async removeTrack(playlistId, position) { - if (invoke) { - try { - return await invoke('playlist_remove_track', { playlistId, position }); - } catch (error) { - console.error('[api.playlists.removeTrack] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('playlist_remove_track', { playlistId, position }); + if (result !== null) return result; return request(`/playlists/${playlistId}/tracks/${position}`, { method: 'DELETE', }); @@ -175,18 +125,12 @@ export const playlists = { * @returns {Promise<{success: boolean}>} */ async reorder(playlistId, fromPosition, toPosition) { - if (invoke) { - try { - return await invoke('playlist_reorder_tracks', { - playlistId, - fromPosition, - toPosition, - }); - } catch (error) { - console.error('[api.playlists.reorder] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('playlist_reorder_tracks', { + playlistId, + fromPosition, + toPosition, + }); + if (result !== null) return result; return request(`/playlists/${playlistId}/tracks/reorder`, { method: 'POST', body: JSON.stringify({ from_position: fromPosition, to_position: toPosition }), @@ -200,14 +144,8 @@ export const playlists = { * @returns {Promise<{success: boolean}>} */ async reorderPlaylists(fromPosition, toPosition) { - if (invoke) { - try { - return await invoke('playlists_reorder', { fromPosition, toPosition }); - } catch (error) { - console.error('[api.playlists.reorderPlaylists] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('playlists_reorder', { fromPosition, toPosition }); + if (result !== null) return result; return request('/playlists/reorder', { method: 'POST', body: JSON.stringify({ from_position: fromPosition, to_position: toPosition }), diff --git a/app/frontend/js/api/queue.js b/app/frontend/js/api/queue.js index 71bc751d..22df5b17 100644 --- a/app/frontend/js/api/queue.js +++ b/app/frontend/js/api/queue.js @@ -4,7 +4,7 @@ * Playback queue management: add, remove, reorder, shuffle, and state. */ -import { ApiError, invoke, request } from './shared.js'; +import { request, tauriInvoke } from './shared.js'; export const queue = { /** @@ -12,14 +12,8 @@ export const queue = { * @returns {Promise<{items: Array, count: number}>} Queue response */ async get() { - if (invoke) { - try { - return await invoke('queue_get'); - } catch (error) { - console.error('[api.queue.get] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('queue_get'); + if (result !== null) return result; return request('/queue'); }, @@ -31,17 +25,11 @@ export const queue = { */ async add(trackIds, position) { const ids = Array.isArray(trackIds) ? trackIds : [trackIds]; - if (invoke) { - try { - return await invoke('queue_add', { - trackIds: ids, - position: position ?? null, - }); - } catch (error) { - console.error('[api.queue.add] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('queue_add', { + trackIds: ids, + position: position ?? null, + }); + if (result !== null) return result; return request('/queue/add', { method: 'POST', body: JSON.stringify({ track_ids: ids, position }), @@ -55,17 +43,11 @@ export const queue = { * @returns {Promise<{added: number, queue_length: number, tracks: Array}>} */ async addFiles(filepaths, position) { - if (invoke) { - try { - return await invoke('queue_add_files', { - filepaths, - position: position ?? null, - }); - } catch (error) { - console.error('[api.queue.addFiles] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('queue_add_files', { + filepaths, + position: position ?? null, + }); + if (result !== null) return result; return request('/queue/add-files', { method: 'POST', body: JSON.stringify({ filepaths, position }), @@ -78,14 +60,8 @@ export const queue = { * @returns {Promise} */ async remove(position) { - if (invoke) { - try { - return await invoke('queue_remove', { position }); - } catch (error) { - console.error('[api.queue.remove] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('queue_remove', { position }); + if (result !== null) return result; return request(`/queue/${position}`, { method: 'DELETE', }); @@ -96,14 +72,8 @@ export const queue = { * @returns {Promise} */ async clear() { - if (invoke) { - try { - return await invoke('queue_clear'); - } catch (error) { - console.error('[api.queue.clear] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('queue_clear'); + if (result !== null) return result; return request('/queue/clear', { method: 'POST', }); @@ -116,17 +86,12 @@ export const queue = { * @returns {Promise<{success: boolean, queue_length: number}>} */ async move(from, to) { - if (invoke) { - try { - return await invoke('queue_reorder', { - fromPosition: from, - toPosition: to, - }); - } catch (error) { - console.error('[api.queue.move] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('queue_reorder', { + fromPosition: from, + toPosition: to, + }); + if (result !== null) return result; + return request('/queue/reorder', { method: 'POST', body: JSON.stringify({ from_position: from, to_position: to }), @@ -139,14 +104,8 @@ export const queue = { * @returns {Promise<{success: boolean, queue_length: number}>} */ async shuffle(keepCurrent = true) { - if (invoke) { - try { - return await invoke('queue_shuffle', { keepCurrent }); - } catch (error) { - console.error('[api.queue.shuffle] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('queue_shuffle', { keepCurrent }); + if (result !== null) return result; return request('/queue/shuffle', { method: 'POST', body: JSON.stringify({ keep_current: keepCurrent }), @@ -163,18 +122,12 @@ export const queue = { * @returns {Promise<{items: Array, current_index: number, track: Object, shuffle_enabled: boolean}>} */ async playContext(trackIds, startIndex, shuffle) { - if (invoke) { - try { - return await invoke('queue_play_context', { - trackIds, - startIndex, - shuffle, - }); - } catch (error) { - console.error('[api.queue.playContext] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('queue_play_context', { + trackIds, + startIndex, + shuffle, + }); + if (result !== null) return result; return request('/queue/play-context', { method: 'POST', body: JSON.stringify({ @@ -194,15 +147,9 @@ export const queue = { * @returns {Promise<{current_index: number, shuffle_enabled: boolean, loop_mode: string, original_order_json: string|null}>} */ async getPlaybackState() { - if (invoke) { - try { - return await invoke('queue_get_playback_state'); - } catch (error) { - console.error('[api.queue.getPlaybackState] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } - throw new ApiError(500, 'Queue playback state not available in browser mode'); + const result = await tauriInvoke('queue_get_playback_state'); + if (result !== null) return result; + throw new Error('Queue playback state not available in browser mode'); }, /** @@ -211,14 +158,8 @@ export const queue = { * @returns {Promise} */ async setCurrentIndex(index) { - if (invoke) { - try { - return await invoke('queue_set_current_index', { index }); - } catch (error) { - console.error('[api.queue.setCurrentIndex] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('queue_set_current_index', { index }); + if (result !== null) return result; console.debug('Queue setCurrentIndex (no-op in browser):', index); }, @@ -228,14 +169,8 @@ export const queue = { * @returns {Promise} State snapshot with reordered queue */ async setShuffle(enabled) { - if (invoke) { - try { - return await invoke('queue_set_shuffle', { enabled }); - } catch (error) { - console.error('[api.queue.setShuffle] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('queue_set_shuffle', { enabled }); + if (result !== null) return result; console.debug('Queue setShuffle (no-op in browser):', enabled); }, @@ -245,14 +180,8 @@ export const queue = { * @returns {Promise} */ async setLoop(mode) { - if (invoke) { - try { - return await invoke('queue_set_loop', { mode }); - } catch (error) { - console.error('[api.queue.setLoop] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('queue_set_loop', { mode }); + if (result !== null) return result; console.debug('Queue setLoop (no-op in browser):', mode); }, @@ -262,15 +191,9 @@ export const queue = { * @returns {Promise} */ async addPlayNext(trackIds) { - if (invoke) { - try { - return await invoke('queue_add_play_next', { trackIds }); - } catch (error) { - console.error('[api.queue.addPlayNext] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } - throw new ApiError(500, 'addPlayNext not available in browser mode'); + const result = await tauriInvoke('queue_add_play_next', { trackIds }); + if (result !== null) return result; + throw new Error('addPlayNext not available in browser mode'); }, /** @@ -278,15 +201,9 @@ export const queue = { * @returns {Promise} */ async playNextTrack() { - if (invoke) { - try { - return await invoke('queue_play_next_track'); - } catch (error) { - console.error('[api.queue.playNextTrack] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } - throw new ApiError(500, 'playNextTrack not available in browser mode'); + const result = await tauriInvoke('queue_play_next_track'); + if (result !== null) return result; + throw new Error('playNextTrack not available in browser mode'); }, /** @@ -295,15 +212,9 @@ export const queue = { * @returns {Promise} */ async playPreviousTrack(currentTimeMs) { - if (invoke) { - try { - return await invoke('queue_play_previous_track', { currentTimeMs }); - } catch (error) { - console.error('[api.queue.playPreviousTrack] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } - throw new ApiError(500, 'playPreviousTrack not available in browser mode'); + const result = await tauriInvoke('queue_play_previous_track', { currentTimeMs }); + if (result !== null) return result; + throw new Error('playPreviousTrack not available in browser mode'); }, /** @@ -311,15 +222,9 @@ export const queue = { * @returns {Promise} */ async skipNext() { - if (invoke) { - try { - return await invoke('queue_skip_next'); - } catch (error) { - console.error('[api.queue.skipNext] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } - throw new ApiError(500, 'skipNext not available in browser mode'); + const result = await tauriInvoke('queue_skip_next'); + if (result !== null) return result; + throw new Error('skipNext not available in browser mode'); }, /** @@ -328,15 +233,9 @@ export const queue = { * @returns {Promise} */ async skipPrevious(currentTimeMs) { - if (invoke) { - try { - return await invoke('queue_skip_previous', { currentTimeMs }); - } catch (error) { - console.error('[api.queue.skipPrevious] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } - throw new ApiError(500, 'skipPrevious not available in browser mode'); + const result = await tauriInvoke('queue_skip_previous', { currentTimeMs }); + if (result !== null) return result; + throw new Error('skipPrevious not available in browser mode'); }, /** @@ -344,14 +243,8 @@ export const queue = { * @returns {Promise} */ async checkIntegrity() { - if (invoke) { - try { - return await invoke('queue_check_integrity'); - } catch (error) { - console.error('[api.queue.checkIntegrity] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } - throw new ApiError(500, 'checkIntegrity not available in browser mode'); + const result = await tauriInvoke('queue_check_integrity'); + if (result !== null) return result; + throw new Error('checkIntegrity not available in browser mode'); }, }; diff --git a/app/frontend/js/api/settings.js b/app/frontend/js/api/settings.js index cd3a1399..1fbd0cb3 100644 --- a/app/frontend/js/api/settings.js +++ b/app/frontend/js/api/settings.js @@ -4,7 +4,7 @@ * Application settings: get, set, update, and reset via Tauri Store. */ -import { ApiError, invoke, request } from './shared.js'; +import { request, tauriInvoke } from './shared.js'; export const settings = { /** @@ -12,15 +12,8 @@ export const settings = { * @returns {Promise<{settings: object}>} */ async getAll() { - if (invoke) { - try { - return await invoke('settings_get_all'); - } catch (error) { - console.error('[api.settings.getAll] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } - // Fallback to HTTP (for backwards compatibility) + const result = await tauriInvoke('settings_get_all'); + if (result !== null) return result; return request('/settings'); }, @@ -30,14 +23,8 @@ export const settings = { * @returns {Promise<{key: string, value: any}>} */ async get(key) { - if (invoke) { - try { - return await invoke('settings_get', { key }); - } catch (error) { - console.error('[api.settings.get] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('settings_get', { key }); + if (result !== null) return result; return request(`/settings/${encodeURIComponent(key)}`); }, @@ -48,14 +35,8 @@ export const settings = { * @returns {Promise<{key: string, value: any}>} */ async set(key, value) { - if (invoke) { - try { - return await invoke('settings_set', { key, value }); - } catch (error) { - console.error('[api.settings.set] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('settings_set', { key, value }); + if (result !== null) return result; return request(`/settings/${encodeURIComponent(key)}`, { method: 'PUT', body: JSON.stringify({ value }), @@ -74,14 +55,8 @@ export const settings = { * @returns {Promise<{updated: string[]}>} */ async update(settings) { - if (invoke) { - try { - return await invoke('settings_update', { settings }); - } catch (error) { - console.error('[api.settings.update] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('settings_update', { settings }); + if (result !== null) return result; return request('/settings', { method: 'PUT', body: JSON.stringify(settings), @@ -93,14 +68,8 @@ export const settings = { * @returns {Promise<{settings: object}>} */ async reset() { - if (invoke) { - try { - return await invoke('settings_reset'); - } catch (error) { - console.error('[api.settings.reset] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } + const result = await tauriInvoke('settings_reset'); + if (result !== null) return result; return request('/settings/reset', { method: 'POST', }); diff --git a/app/frontend/js/api/shared.js b/app/frontend/js/api/shared.js index 02f581ac..3db7caf7 100644 --- a/app/frontend/js/api/shared.js +++ b/app/frontend/js/api/shared.js @@ -57,3 +57,19 @@ export async function request(endpoint, options = {}) { throw new ApiError(0, `Network error: ${error.message}`); } } + +/** + * Invoke a Tauri command with error handling + * @param {string} cmd - Tauri command name + * @param {object} params - Command parameters + * @returns {Promise} Command result + */ +export async function tauriInvoke(cmd, params = {}) { + if (!invoke) return null; + try { + return await invoke(cmd, params); + } catch (error) { + console.error(`[api.tauriInvoke] Tauri error (${cmd}):`, error); + throw new ApiError(500, error.toString()); + } +} diff --git a/app/frontend/js/api/stats.js b/app/frontend/js/api/stats.js index b958a0de..dc81f07f 100644 --- a/app/frontend/js/api/stats.js +++ b/app/frontend/js/api/stats.js @@ -4,7 +4,7 @@ * Listening statistics and chart grid generation. */ -import { ApiError, invoke } from './shared.js'; +import { tauriInvoke } from './shared.js'; export const stats = { /** @@ -13,15 +13,9 @@ export const stats = { * @returns {Promise<{total_plays: number, total_tracks_played: number, total_artists_played: number, total_listening_time: number}>} */ async getOverview(range = 'AllTime') { - if (invoke) { - try { - return await invoke('stats_get_overview', { range }); - } catch (error) { - console.error('[api.stats.getOverview] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } - throw new ApiError(0, 'Stats require Tauri runtime'); + const result = await tauriInvoke('stats_get_overview', { range }); + if (result !== null) return result; + throw new Error('Stats require Tauri runtime'); }, /** @@ -31,15 +25,9 @@ export const stats = { * @returns {Promise>} */ async getTopArtists(range = 'AllTime', limit = 25) { - if (invoke) { - try { - return await invoke('stats_get_top_artists', { range, limit }); - } catch (error) { - console.error('[api.stats.getTopArtists] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } - throw new ApiError(0, 'Stats require Tauri runtime'); + const result = await tauriInvoke('stats_get_top_artists', { range, limit }); + if (result !== null) return result; + throw new Error('Stats require Tauri runtime'); }, /** @@ -49,15 +37,9 @@ export const stats = { * @returns {Promise>} */ async getGenres(range = 'AllTime', limit = 20) { - if (invoke) { - try { - return await invoke('stats_get_genres', { range, limit }); - } catch (error) { - console.error('[api.stats.getGenres] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } - throw new ApiError(0, 'Stats require Tauri runtime'); + const result = await tauriInvoke('stats_get_genres', { range, limit }); + if (result !== null) return result; + throw new Error('Stats require Tauri runtime'); }, /** @@ -66,15 +48,9 @@ export const stats = { * @returns {Promise>} */ async getPlaysOverTime(range = 'AllTime') { - if (invoke) { - try { - return await invoke('stats_get_plays_over_time', { range }); - } catch (error) { - console.error('[api.stats.getPlaysOverTime] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } - throw new ApiError(0, 'Stats require Tauri runtime'); + const result = await tauriInvoke('stats_get_plays_over_time', { range }); + if (result !== null) return result; + throw new Error('Stats require Tauri runtime'); }, /** @@ -89,14 +65,8 @@ export const stats = { * @returns {Promise} Data URL (data:image/png;base64,...) */ async generateChartGrid(request) { - if (invoke) { - try { - return await invoke('stats_generate_chart_grid', { request }); - } catch (error) { - console.error('[api.stats.generateChartGrid] Tauri error:', error); - throw new ApiError(500, error.toString()); - } - } - throw new ApiError(0, 'Stats require Tauri runtime'); + const result = await tauriInvoke('stats_generate_chart_grid', { request }); + if (result !== null) return result; + throw new Error('Stats require Tauri runtime'); }, }; diff --git a/crates/mt-tauri/src/agent/setup.rs b/crates/mt-tauri/src/agent/setup.rs index 8faf5034..ea16c77b 100644 --- a/crates/mt-tauri/src/agent/setup.rs +++ b/crates/mt-tauri/src/agent/setup.rs @@ -62,6 +62,8 @@ pub async fn pull_model(app: &tauri::AppHandle, model: String) -> Result Result= LOG_INTERVAL { + last_log = std::time::Instant::now(); + if let (Some(completed), Some(total)) = (progress.completed, progress.total) { + let pct = (completed as f64 / total as f64 * 100.0) as u32; + info!("{}% - {}", pct, progress.status); + } else { + info!("{}", progress.status); + } + } let _ = app.emit("agent://pull-progress", &progress); } } diff --git a/crates/mt-tauri/src/agent/tools.rs b/crates/mt-tauri/src/agent/tools.rs index 4b7f1bb5..69a7460b 100644 --- a/crates/mt-tauri/src/agent/tools.rs +++ b/crates/mt-tauri/src/agent/tools.rs @@ -26,6 +26,19 @@ use serde::{Deserialize, Serialize}; use super::types::{AgentContext, AgentError, TrackSummary}; use crate::db::{favorites, library, library::LibraryQuery, models::StatsDateRange, stats}; +/// Helper to construct a `ToolDefinition` from static metadata. +fn tool_def( + name: &'static str, + description: &'static str, + parameters: serde_json::Value, +) -> ToolDefinition { + ToolDefinition { + name: name.into(), + description: description.into(), + parameters, + } +} + // --------------------------------------------------------------------------- // ToolOutput — wrapper for tool results with actionable hints on empty results // --------------------------------------------------------------------------- @@ -89,19 +102,17 @@ impl Tool for GetRecentlyPlayed { type Output = ToolOutput; async fn definition(&self, _prompt: String) -> ToolDefinition { - ToolDefinition { - name: Self::NAME.into(), - description: - "Get tracks the user played recently. Use to understand current listening habits." - .into(), - parameters: serde_json::json!({ + tool_def( + Self::NAME, + "Get tracks the user played recently. Use to understand current listening habits.", + serde_json::json!({ "type": "object", "properties": { "days": { "type": "integer", "description": "Number of days to look back (default: 7)" }, "limit": { "type": "integer", "description": "Max tracks to return (default: 20)" } } }), - } + ) } async fn call(&self, args: Self::Args) -> Result { @@ -170,12 +181,10 @@ impl Tool for GetTopArtists { type Output = ToolOutput; async fn definition(&self, _prompt: String) -> ToolDefinition { - ToolDefinition { - name: Self::NAME.into(), - description: - "Get the user's most-played artists. Use to understand long-term preferences." - .into(), - parameters: serde_json::json!({ + tool_def( + Self::NAME, + "Get the user's most-played artists. Use to understand long-term preferences.", + serde_json::json!({ "type": "object", "properties": { "range": { @@ -186,7 +195,7 @@ impl Tool for GetTopArtists { "limit": { "type": "integer", "description": "Max artists to return (default: 10)" } } }), - } + ) } async fn call(&self, args: Self::Args) -> Result { @@ -266,10 +275,10 @@ impl Tool for SearchLibrary { type Output = ToolOutput; async fn definition(&self, _prompt: String) -> ToolDefinition { - ToolDefinition { - name: Self::NAME.into(), - description: "Search the user's music library by keyword, artist, album, genre, or year range. Returns matching tracks with year metadata.".into(), - parameters: serde_json::json!({ + tool_def( + Self::NAME, + "Search the user's music library by keyword, artist, album, genre, or year range. Returns matching tracks with year metadata.", + serde_json::json!({ "type": "object", "properties": { "query": { "type": "string", "description": "Free-text search across title, artist, album" }, @@ -282,7 +291,7 @@ impl Tool for SearchLibrary { "limit": { "type": "integer", "description": "Max tracks to return (default: 20)" } } }), - } + ) } async fn call(&self, args: Self::Args) -> Result { @@ -345,12 +354,10 @@ impl Tool for GetSimilarTracks { type Output = ToolOutput; async fn definition(&self, _prompt: String) -> ToolDefinition { - ToolDefinition { - name: Self::NAME.into(), - description: - "Find tracks similar to a given track. Returns only tracks in the user's library." - .into(), - parameters: serde_json::json!({ + tool_def( + Self::NAME, + "Find tracks similar to a given track. Returns only tracks in the user's library.", + serde_json::json!({ "type": "object", "properties": { "artist": { "type": "string", "description": "Artist of the seed track" }, @@ -359,7 +366,7 @@ impl Tool for GetSimilarTracks { }, "required": ["artist", "track"] }), - } + ) } async fn call(&self, args: Self::Args) -> Result { @@ -455,10 +462,10 @@ impl Tool for GetSimilarArtists { type Output = ToolOutput; async fn definition(&self, _prompt: String) -> ToolDefinition { - ToolDefinition { - name: Self::NAME.into(), - description: "Find artists similar to a given artist. Returns only artists in the user's library with sample tracks.".into(), - parameters: serde_json::json!({ + tool_def( + Self::NAME, + "Find artists similar to a given artist. Returns only artists in the user's library with sample tracks.", + serde_json::json!({ "type": "object", "properties": { "artist": { "type": "string", "description": "Artist to find similar artists for" }, @@ -466,7 +473,7 @@ impl Tool for GetSimilarArtists { }, "required": ["artist"] }), - } + ) } async fn call(&self, args: Self::Args) -> Result { @@ -572,11 +579,10 @@ impl Tool for GetTrackTags { type Output = ToolOutput; async fn definition(&self, _prompt: String) -> ToolDefinition { - ToolDefinition { - name: Self::NAME.into(), - description: "Get mood and genre tags for a track. Use to understand a track's vibe." - .into(), - parameters: serde_json::json!({ + tool_def( + Self::NAME, + "Get mood and genre tags for a track. Use to understand a track's vibe.", + serde_json::json!({ "type": "object", "properties": { "artist": { "type": "string", "description": "Artist of the track" }, @@ -584,7 +590,7 @@ impl Tool for GetTrackTags { }, "required": ["artist", "track"] }), - } + ) } async fn call(&self, args: Self::Args) -> Result { @@ -654,10 +660,10 @@ impl Tool for GetTopArtistsByTag { type Output = ToolOutput; async fn definition(&self, _prompt: String) -> ToolDefinition { - ToolDefinition { - name: Self::NAME.into(), - description: "Find top artists in a genre/tag. Returns only artists in the user's library with sample tracks.".into(), - parameters: serde_json::json!({ + tool_def( + Self::NAME, + "Find top artists in a genre/tag. Returns only artists in the user's library with sample tracks.", + serde_json::json!({ "type": "object", "properties": { "tag": { "type": "string", "description": "Genre or tag (e.g. shoegaze, jazz, indie rock)" }, @@ -665,7 +671,7 @@ impl Tool for GetTopArtistsByTag { }, "required": ["tag"] }), - } + ) } async fn call(&self, args: Self::Args) -> Result { @@ -771,12 +777,10 @@ impl Tool for GetTopTracksByCountry { type Output = ToolOutput; async fn definition(&self, _prompt: String) -> ToolDefinition { - ToolDefinition { - name: Self::NAME.into(), - description: - "Find trending tracks in a country. Returns only tracks in the user's library." - .into(), - parameters: serde_json::json!({ + tool_def( + Self::NAME, + "Find trending tracks in a country. Returns only tracks in the user's library.", + serde_json::json!({ "type": "object", "properties": { "country": { "type": "string", "description": "Country name (e.g. Japan, Brazil, Germany)" }, @@ -784,7 +788,7 @@ impl Tool for GetTopTracksByCountry { }, "required": ["country"] }), - } + ) } async fn call(&self, args: Self::Args) -> Result { diff --git a/crates/mt-tauri/src/audio/device_isolation.rs b/crates/mt-tauri/src/audio/device_isolation.rs index 2fbb5ab2..4c96dcfd 100644 --- a/crates/mt-tauri/src/audio/device_isolation.rs +++ b/crates/mt-tauri/src/audio/device_isolation.rs @@ -4,6 +4,7 @@ use rodio::cpal::traits::{DeviceTrait, HostTrait}; use std::panic::AssertUnwindSafe; use std::sync::mpsc; use std::time::Duration; +#[cfg(not(test))] use tracing::{debug, warn}; /// Print device names as JSON to stdout and exit. diff --git a/crates/mt-tauri/src/commands/audio.rs b/crates/mt-tauri/src/commands/audio.rs index 1167a50d..3db56841 100644 --- a/crates/mt-tauri/src/commands/audio.rs +++ b/crates/mt-tauri/src/commands/audio.rs @@ -140,7 +140,7 @@ impl AudioState { cache: &NetworkFileCache, app: &AppHandle, ) -> Result { - let resolved = resolve_cached_path_inner(path, cache, app); + let resolved = resolve_cached_path(path, cache, app); let (tx, rx) = mpsc::channel(); self.send_command(AudioCommand::LoadAndPlay(resolved, track_id, tx)); rx.recv().map_err(|_| "Channel closed".to_string())? @@ -455,13 +455,7 @@ fn audio_thread(rx: Receiver, app: AppHandle) { /// If network caching is enabled and the path is on a network mount, /// copy the file to the local cache and return the cached path. /// Otherwise return the original path unchanged. -fn resolve_cached_path(path: &str, cache: &State, app: &AppHandle) -> String { - resolve_cached_path_inner(path, cache, app) -} - -/// Inner implementation that accepts `&NetworkFileCache` directly, -/// callable from other commands without a `State` wrapper. -fn resolve_cached_path_inner(path: &str, cache: &NetworkFileCache, app: &AppHandle) -> String { +fn resolve_cached_path(path: &str, cache: &NetworkFileCache, app: &AppHandle) -> String { let enabled = app .store("settings.json") .ok()