package backend import ( "context" "strings" "sync" ) // SoundtrackData holds precomputed search data for a soundtrack type SoundtrackData struct { Original string Lowercase string Cleaned string } // SearchCache holds precomputed search data for all soundtrack names. type SearchCache struct { load func(context.Context) ([]string, error) mu sync.Mutex cache []SoundtrackData } // NewSearchCache creates a SearchCache that gets the soundtrack names from load. func NewSearchCache(load func(context.Context) ([]string, error)) *SearchCache { return &SearchCache{load: load} } // Get returns the precomputed search data, loading it if needed. func (s *SearchCache) Get(ctx context.Context) ([]SoundtrackData, error) { s.mu.Lock() defer s.mu.Unlock() if len(s.cache) == 0 { if err := s.reload(ctx); err != nil { return nil, err } } return s.cache, nil } // Reload reloads the search data. Call it after a sync. func (s *SearchCache) Reload(ctx context.Context) error { s.mu.Lock() defer s.mu.Unlock() return s.reload(ctx) } func (s *SearchCache) reload(ctx context.Context) error { soundtracks, err := s.load(ctx) if err != nil { return err } cache := make([]SoundtrackData, len(soundtracks)) for i, soundtrack := range soundtracks { cache[i] = SoundtrackData{ Original: soundtrack, Lowercase: strings.ToLower(soundtrack), Cleaned: CleanSearchTerm(soundtrack), } } s.cache = cache return nil } var searchReplacer = strings.NewReplacer(" ", "", "é", "e", "+", "plus", "&", "and", "'n", "and") // CleanSearchTerm normalises a search term or soundtrack name for matching. func CleanSearchTerm(term string) string { return searchReplacer.Replace(strings.ToLower(term)) } // MatchesSubsequence reports whether all characters of term appear in text in the same order. func MatchesSubsequence(term, text string) bool { rest := text for _, r := range term { i := strings.IndexRune(rest, r) if i < 0 { return false } rest = rest[i+len(string(r)):] } return true }