diff --git a/backend/app.go b/backend/app.go index 68fcdff..b72d72f 100644 --- a/backend/app.go +++ b/backend/app.go @@ -118,9 +118,6 @@ func NewYellowJacketApp( yjApp.assetHandler.RegisterHandler("/artist-images/", artistImgHandler) } - // Register local search endpoint — bypasses Wails RPC serialization. - yjApp.assetHandler.RegisterHandler("/api/search-local", yjApp.explore.SearchLocalHandler()) - // create playlist service yjApp.playlist = playlist.NewService( yjApp.logger, yjApp.database, yjApp.appConfig, diff --git a/backend/explore/explore.go b/backend/explore/explore.go index aa76c5b..075d21a 100644 --- a/backend/explore/explore.go +++ b/backend/explore/explore.go @@ -5,7 +5,6 @@ import ( "encoding/json" "log/slog" "math" - "net/http" "sort" "strings" "sync" @@ -155,38 +154,6 @@ func (e *Service) SearchLocal(query string) *MBSearchResult { return &result } -// SearchLocalHandler returns an http.Handler that serves local -// index search results as JSON. This bypasses the Wails RPC -// serialization queue, ensuring sub-millisecond response times. -func (e *Service) SearchLocalHandler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer func() { - if rv := recover(); rv != nil { - e.logger.Error("search-local handler panic", "recover", rv) - http.Error(w, "internal error", http.StatusInternalServerError) - } - }() - - query := r.URL.Query().Get("q") - if query == "" { - w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte("null")) - return - } - - result := e.SearchLocal(query) - - w.Header().Set("Content-Type", "application/json") - - if result == nil { - _, _ = w.Write([]byte("null")) - return - } - - _ = json.NewEncoder(w).Encode(result) - }) -} - // --------------------------------------------------------------------------- // MusicBrainz lookup // --------------------------------------------------------------------------- diff --git a/frontend/src/components/explore-view/explore-view.ts b/frontend/src/components/explore-view/explore-view.ts index be6b994..dff7307 100644 --- a/frontend/src/components/explore-view/explore-view.ts +++ b/frontend/src/components/explore-view/explore-view.ts @@ -9,6 +9,7 @@ import type { MBReleaseGroup, MBRecording, } from '@go/explore/Service'; +import { libraryStore } from '../../store/library-store'; import '@awesome.me/webawesome/dist/components/icon/icon.js'; /* ── Constants ── */ @@ -520,35 +521,74 @@ export class ExploreView extends LitElement { const startTime = performance.now(); console.log(`[explore] search started: "${query}"`); - // Phase 1: fetch local index results via HTTP (bypasses Wails - // RPC serialization — guaranteed instant). - try { - const resp = await fetch(`/api/search-local?q=${encodeURIComponent(query)}`); - if (version !== this.searchVersion) return; - if (resp.ok) { - const local = await resp.json() as MBSearchResult | null; - if (local && (local.artists?.length || local.releaseGroups?.length || local.recordings?.length)) { - this.results = local; - this.loadThumbnails(); - this.loadArtistImages(); - this.checkLibrary(); - const elapsed = (performance.now() - startTime).toFixed(0); - console.log( - `[explore] local results: "${query}" in ${elapsed}ms — ` + - `artists=${local.artists?.length ?? 0}, ` + - `albums=${local.releaseGroups?.length ?? 0}, ` + - `tracks=${local.recordings?.length ?? 0}`, - ); - } - } - } catch { - // Local search failed — continue to full search. + // Phase 1: instant library search — pure frontend, no Go calls. + const localResults = this.searchLibraryCache(query); + if (localResults && (localResults.artists?.length || localResults.releaseGroups?.length)) { + this.results = localResults; + this.loadThumbnails(); + this.loadArtistImages(); + const elapsed = (performance.now() - startTime).toFixed(0); + console.log( + `[explore] library results: "${query}" in ${elapsed}ms — ` + + `artists=${localResults.artists?.length ?? 0}, ` + + `albums=${localResults.releaseGroups?.length ?? 0}`, + ); } // Phase 2: full pipeline (MB + LB + reranking) via Wails RPC. void this.executeFullSearch(version, query, startTime); } + /** + * Search the frontend library cache for matching artists and albums. + * Pure JS — no Go calls, guaranteed instant. + */ + private searchLibraryCache(query: string): MBSearchResult | null { + const q = query.toLowerCase(); + + const artists: MBArtist[] = []; + const cachedArtists = libraryStore.cachedArtists; + if (cachedArtists) { + for (const a of cachedArtists) { + if (a.Name.toLowerCase().includes(q)) { + artists.push({ + mbid: '', + name: a.Name, + sortName: '', + type: 'Group', + country: '', + disambiguation: '', + score: 100, + } as MBArtist); + if (artists.length >= 5) break; + } + } + } + + const releaseGroups: MBReleaseGroup[] = []; + const cachedAlbums = libraryStore.cachedAlbums; + if (cachedAlbums) { + for (const a of cachedAlbums) { + if (a.Name.toLowerCase().includes(q) || a.ArtistName.toLowerCase().includes(q)) { + releaseGroups.push({ + mbid: '', + title: a.Name, + primaryType: 'Album', + artistCredit: a.ArtistName, + firstReleaseDate: a.Year ? String(a.Year) : '', + } as MBReleaseGroup); + if (releaseGroups.length >= 5) break; + } + } + } + + if (artists.length === 0 && releaseGroups.length === 0) { + return null; + } + + return { artists, releaseGroups, recordings: [] } as MBSearchResult; + } + private async executeFullSearch(version: number, query: string, startTime: number) { try { const result = await Search(query); diff --git a/frontend/src/store/library-store.ts b/frontend/src/store/library-store.ts index 7c9d16a..0b7c8e4 100644 --- a/frontend/src/store/library-store.ts +++ b/frontend/src/store/library-store.ts @@ -120,6 +120,16 @@ class LibraryStore { // Returns cached data or fetches from backend on first access. // =================================================================== + /** Synchronous access to cached artists (null if not yet loaded). */ + get cachedArtists(): library.Artist[] | null { + return this.artists; + } + + /** Synchronous access to cached albums (null if not yet loaded). */ + get cachedAlbums(): library.Album[] | null { + return this.albums; + } + async getTracks(): Promise { if (this.tracks !== null) { return this.tracks;