package web import ( "net/http" "strings" "music-server/internal/backend" "music-server/internal/logging" "go.uber.org/zap" ) // NewSearchHandler returns a handler that renders the soundtracks matching the posted search term. func NewSearchHandler(cache *backend.SearchCache) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "Bad Request", http.StatusBadRequest) return } soundtracks, err := cache.Get(r.Context()) if err != nil { logging.GetLogger().Error("Failed to load search data", zap.Error(err)) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } results := search(r.FormValue("search_term"), soundtracks) if err := FoundSoundtracks(results).Render(r.Context(), w); err != nil { logging.GetLogger().Error("Failed to render search results", zap.Error(err)) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } }) } // SearchResult is a soundtrack found by search. MatchedName is the alternative name that // matched, or empty when the soundtrack's own name matched. type SearchResult struct { Name string MatchedName string } // search returns the soundtracks matching searchText: exact matches first, then matches // ignoring spaces and special characters, then matches with the letters in order. Each // step tries the soundtrack's own name before its alternative names. func search(searchText string, soundtrackData []backend.SoundtrackData) []SearchResult { var results []SearchResult seen := make(map[string]bool) add := func(match func(backend.SearchName) bool) { for _, data := range soundtrackData { if seen[data.Original] { continue } matched, ok := "", match(data.SearchName) for _, alternative := range data.Alternatives { if ok { break } matched, ok = alternative.Original, match(alternative) } if ok { results = append(results, SearchResult{Name: data.Original, MatchedName: matched}) seen[data.Original] = true } } } lowercase := strings.ToLower(searchText) cleaned := backend.CleanSearchTerm(searchText) add(func(n backend.SearchName) bool { return strings.Contains(n.Lowercase, lowercase) }) add(func(n backend.SearchName) bool { return strings.Contains(n.Cleaned, cleaned) }) add(func(n backend.SearchName) bool { return backend.MatchesSubsequence(cleaned, n.Cleaned) }) return results }