Files
yellowjacket/backend/library/exclusions.go
T
logan acbe7c4676 feat(library): remove tracks from the library without touching the file
"Remove from library" deletes the audio_files row and records the path
as excluded, so the next scan does not import it again. Without the
exclusion the operation undoes itself on the next scan, which is worse
than not having it at all; the file on disk is never touched, which is
the promise the confirmation copy will make.

The soft scan compares the number of audio files on disk against the
number of rows, so both walks now skip excluded paths — otherwise the
two counts disagree forever and every launch queues a full scan of the
whole library. A full rescan clears the exclusions, which is the only
way back for a path removed by mistake until there is a UI for it.
2026-08-13 13:04:59 -04:00

93 lines
2.4 KiB
Go

package library
import (
"fmt"
"path/filepath"
"strings"
"yellowjacket/backend/database/sql/sqlcgen"
)
// libraryRoot is one configured library's id and root directory.
type libraryRoot struct {
id int64
path string
}
// libraryRoots returns every configured library's id and root path.
func (l *Library) libraryRoots() ([]libraryRoot, error) {
libs, err := l.db.Queries.GetAllLibraries(l.ctx)
if err != nil {
return nil, fmt.Errorf("could not load libraries: %w", err)
}
roots := make([]libraryRoot, 0, len(libs))
for _, lib := range libs {
roots = append(roots, libraryRoot{id: lib.ID, path: lib.Path})
}
return roots, nil
}
// pathWithin reports whether path lies under root. Both are cleaned
// first, and the comparison keeps the separator so /music/rock does not
// swallow /music/rockabilly.
func pathWithin(root, path string) bool {
cleanRoot := filepath.Clean(root)
cleanPath := filepath.Clean(path)
if cleanRoot == cleanPath {
return true
}
return strings.HasPrefix(cleanPath, cleanRoot+string(filepath.Separator))
}
// excludeParams is the insert parameter for one exclusion.
func excludeParams(libraryID int64, filePath string) sqlcgen.ExcludePathParams {
return sqlcgen.ExcludePathParams{
LibraryID: libraryID,
FilePath: filePath,
}
}
// excludedPathSet loads a library's excluded paths as a set, cleaned
// the same way the walk builds its absolute paths so the two compare.
//
// A failure here returns an empty set and logs: a scan that cannot read
// the exclusions imports what it finds, which is the pre-exclusion
// behaviour rather than an empty library.
func (l *Library) excludedPathSet(libraryID int64) map[string]struct{} {
paths, err := l.db.Queries.GetExcludedPathsByLibrary(l.ctx, libraryID)
if err != nil {
l.logger.Warn("could not load excluded paths, scanning everything",
"libraryID", libraryID, "err", err)
return nil
}
if len(paths) == 0 {
return nil
}
set := make(map[string]struct{}, len(paths))
for _, p := range paths {
set[filepath.Clean(p)] = struct{}{}
}
return set
}
// isExcluded reports whether an absolute file path is in the set. A
// nil set excludes nothing, which is what every caller wants when
// there are no exclusions at all.
func isExcluded(set map[string]struct{}, absolutePath string) bool {
if len(set) == 0 {
return false
}
_, found := set[filepath.Clean(absolutePath)]
return found
}