RemoveFromLibrary deletes the audio_files rows the way the scan's own orphan cleanup does and records each path in excluded_paths. The exclusion is not an enhancement: without it the next scan finds the file, sees no row and imports it again, so the button undoes itself. The soft scan compares files on disk against rows in the database, so surveyAudioFiles and countAudioFiles both take the exclusion set — otherwise an excluded path makes the two disagree forever and queues a full scan on every launch. Deleting a row cascades to queue_tracks, so the removal calls the same CompactQueue hook RemoveLibrary does. Also lands the requested badge: library-status-indicator is a button again where it can act, utils/library-status.ts states once what owning and wanting mean, and the long-declared queued state finally has a producer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm
93 lines
2.4 KiB
Go
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
|
|
}
|