package backend import ( "context" "strings" "sync" ) // SearchSoundtrack is a soundtrack's name and alternative names, as loaded for search. type SearchSoundtrack struct { Name string AlternativeNames []string } // SearchName holds a name prepared for matching. type SearchName struct { Original string Lowercase string Cleaned string } func newSearchName(name string) SearchName { return SearchName{Original: name, Lowercase: strings.ToLower(name), Cleaned: CleanSearchTerm(name)} } // SoundtrackData holds precomputed search data for a soundtrack: its name and its // alternative names. type SoundtrackData struct { SearchName Alternatives []SearchName } // SearchCache holds precomputed search data for all soundtracks. type SearchCache struct { load func(context.Context) ([]SearchSoundtrack, error) mu sync.Mutex cache []SoundtrackData } // NewSearchCache creates a SearchCache that gets the soundtracks from load. func NewSearchCache(load func(context.Context) ([]SearchSoundtrack, 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{SearchName: newSearchName(soundtrack.Name)} for _, name := range soundtrack.AlternativeNames { cache[i].Alternatives = append(cache[i].Alternatives, newSearchName(name)) } } 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 }