Security - Serve character images through os.Root so names like ../ can't escape the characters folder - Sync start and /sync/reset are POST only (legacy and /api/v1) - /api/v1/token/cleanup requires a token, password compared in constant time, tokens no longer logged - Echo's rate limiter replaces the custom one; TRUSTED_PROXIES enables X-Forwarded-For for reverse proxies Crashes and races - Sync state lives in SyncService with atomics and a mutex instead of unsynchronised globals (fixes concurrent map writes during full sync) - Only one sync can run; /api/v1 sync and music routes are locked during sync like the legacy ones - No log.Fatal outside startup: errors become 4xx/5xx responses - Search uses an in-order character match instead of unescaped regexes - Empty queue / no songs return 404 instead of panicking or looping Behaviour - Sync result is computed once and stays the same between polls - Swagger and /openapi serve the embedded spec - Shutdown stops HTTP before closing the database - Expired tokens are deleted with a context that outlives the request - Files are served with Range support and correct content types; audio has no write deadline Performance - Migration 000006 adds soundtrack.fingerprint (names, sizes, mtimes) so unchanged folders are skipped without reading any files - Songs are reconciled per soundtrack with one query plus needed writes; renames keep play counts - Moved MUSIC_PATH updates soundtrack and song paths - .id cleanup only runs for changed soundtracks or a full sync - Classic random uses one SQL query; Gitea release lookups are cached with a timeout Structure - internal/config reads all environment variables once - sqlc emits a Querier interface; backend globals become services injected into handlers - One route registration shared by legacy and /api/v1 - Removed dead code (dbHelper.go, unused middleware, duplicate Gitea functions, embedded Swagger UI copy) and lib/pq - templ and swag pinned as go tools
84 lines
2.0 KiB
Go
84 lines
2.0 KiB
Go
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
|
|
}
|