package model import ( "cmp" "path/filepath" "slices" "strings" ) // LibraryMatcher finds the library that contains an absolute path. type LibraryMatcher struct { libraries Libraries cleanedPaths []string } // NewLibraryMatcher sorts the libraries longest path first, so /music-classical is checked before /music. func NewLibraryMatcher(libs Libraries) *LibraryMatcher { libs = slices.Clone(libs) slices.SortFunc(libs, func(i, j Library) int { return cmp.Compare(len(j.Path), len(i.Path)) }) cleanedPaths := make([]string, len(libs)) for i, lib := range libs { cleanedPaths[i] = filepath.Clean(lib.Path) } return &LibraryMatcher{libraries: libs, cleanedPaths: cleanedPaths} } // FindLibrary returns the library whose path contains absolutePath. func (lm *LibraryMatcher) FindLibrary(absolutePath string) (Library, bool) { for i, libPath := range lm.cleanedPaths { // A cleaned path only ends with a separator when it is a filesystem root if strings.HasPrefix(absolutePath, libPath) && (len(absolutePath) == len(libPath) || absolutePath[len(libPath)] == filepath.Separator || strings.HasSuffix(libPath, string(filepath.Separator))) { return lm.libraries[i], true } } return Library{}, false } // LibraryRelativePath rebases an absolute path onto the library root, as the scanner's io/fs sees it // (forward slashes). Relative paths, and absolute paths outside the library root, are returned unchanged. func LibraryRelativePath(libPath, path string) string { if !filepath.IsAbs(path) { return path } // The library root may be relative (e.g. the default "./music"); it resolves against the same cwd absLib, err := filepath.Abs(libPath) if err != nil { return path } rel, err := filepath.Rel(absLib, path) if err != nil || !filepath.IsLocal(rel) { return path } return filepath.ToSlash(rel) }