feat(library): remove a track from the library without deleting the file
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
This commit is contained in:
@@ -334,7 +334,13 @@ func (l *Library) scanInternal(
|
||||
// --- Pre-walk: count audio files for progress reporting ---
|
||||
emitProgress(mkProgress("counting", 0, 0, 0, 0, 0))
|
||||
|
||||
totalFiles := countAudioFiles(basePath)
|
||||
// Paths the user has removed from the library. Loaded once per
|
||||
// scan: the walk consults it per file, and the counts above and
|
||||
// below must agree with it or the progress bar and the soft scan
|
||||
// both describe a library that is not the one being built.
|
||||
excluded := l.excludedPathSet(libraryID)
|
||||
|
||||
totalFiles := countAudioFiles(basePath, excluded)
|
||||
|
||||
l.logger.Debug(
|
||||
"pre-walk file count complete",
|
||||
@@ -449,6 +455,20 @@ func (l *Library) scanInternal(
|
||||
return nil
|
||||
}
|
||||
|
||||
// The user removed this path from the library. Leaving
|
||||
// it out of existingPaths' LoadAndDelete as well is
|
||||
// deliberate: if a row somehow exists for an excluded
|
||||
// path, orphan cleanup below deletes it, which is the
|
||||
// state the user asked for.
|
||||
if isExcluded(excluded, absoluteFilePath) {
|
||||
l.logger.Debug(
|
||||
"skipping excluded path",
|
||||
"path", absoluteFilePath,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stat the entry for the staleness comparison below.
|
||||
// This happens before the file is read, so a file
|
||||
// modified mid-scan records the pre-read mtime and is
|
||||
@@ -1217,21 +1237,27 @@ func (l *Library) pruneOrphanedMetadata() {
|
||||
// countAudioFiles performs a fast walk of the library directory,
|
||||
// counting only files with supported audio extensions. No per-file
|
||||
// I/O is performed — this reads only directory entries.
|
||||
func countAudioFiles(basePath string) int64 {
|
||||
func countAudioFiles(basePath string, excluded map[string]struct{}) int64 {
|
||||
var count int64
|
||||
|
||||
_ = fs.WalkDir(
|
||||
os.DirFS(basePath), ".",
|
||||
func(_ string, d fs.DirEntry, err error) error {
|
||||
func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
ext := filepath.Ext(d.Name())
|
||||
if _, ok := metadata.GetSupportedFileType(ext); ok {
|
||||
count++
|
||||
if _, ok := metadata.GetSupportedFileType(ext); !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if isExcluded(excluded, filepath.Join(basePath, path)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
count++
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
@@ -1248,10 +1274,17 @@ func countAudioFiles(basePath string) int64 {
|
||||
// Unlike countAudioFiles this stats every entry, so it is the more
|
||||
// expensive of the two walks. Only the startup soft scan uses it —
|
||||
// the in-scan progress total does not need mtimes.
|
||||
func surveyAudioFiles(basePath string) (count, maxModTime int64) {
|
||||
//
|
||||
// Both walks take the library's excluded paths and skip them, because
|
||||
// both answer "how many files would a scan import", not "how many
|
||||
// files are there".
|
||||
func surveyAudioFiles(
|
||||
basePath string,
|
||||
excluded map[string]struct{},
|
||||
) (count, maxModTime int64) {
|
||||
_ = fs.WalkDir(
|
||||
os.DirFS(basePath), ".",
|
||||
func(_ string, d fs.DirEntry, err error) error {
|
||||
func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
@@ -1261,6 +1294,14 @@ func surveyAudioFiles(basePath string) (count, maxModTime int64) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// An excluded path is not a file this scan would import,
|
||||
// so it must not be counted: the soft scan compares this
|
||||
// count against the database's, and a permanent
|
||||
// disagreement queues a full scan on every launch.
|
||||
if isExcluded(excluded, filepath.Join(basePath, path)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
count++
|
||||
|
||||
info, infoErr := d.Info()
|
||||
|
||||
Reference in New Issue
Block a user