// Package killist maintains a persistent list of track titles that should // be automatically skipped during playback — a direct port of the Delphi // KillList / KillFile feature. package killist import ( "bufio" "os" "path/filepath" "strings" "sync" ) // KillList is a thread-safe set of track titles to skip. type KillList struct { mu sync.RWMutex titles map[string]struct{} filepath string } // Load reads the kill list from disk (creates an empty list if the file // does not exist yet). func Load(path string) (*KillList, error) { kl := &KillList{ titles: make(map[string]struct{}), filepath: path, } f, err := os.Open(path) if os.IsNotExist(err) { return kl, nil } if err != nil { return nil, err } defer f.Close() sc := bufio.NewScanner(f) for sc.Scan() { line := strings.TrimSpace(sc.Text()) if line != "" { kl.titles[line] = struct{}{} } } return kl, sc.Err() } // Contains reports whether the given title is on the kill list. func (kl *KillList) Contains(title string) bool { kl.mu.RLock() defer kl.mu.RUnlock() _, ok := kl.titles[title] return ok } // Add appends a title and persists the list. func (kl *KillList) Add(title string) error { kl.mu.Lock() defer kl.mu.Unlock() kl.titles[title] = struct{}{} return kl.save() } // Remove removes a title and persists the list. func (kl *KillList) Remove(title string) error { kl.mu.Lock() defer kl.mu.Unlock() delete(kl.titles, title) return kl.save() } // List returns all titles currently on the kill list. func (kl *KillList) List() []string { kl.mu.RLock() defer kl.mu.RUnlock() out := make([]string, 0, len(kl.titles)) for t := range kl.titles { out = append(out, t) } return out } // save writes the kill list atomically: it writes to a temp file in the same // directory, flushes, closes, then renames over the target. This ensures the // existing file is never truncated on a partial write or flush error. func (kl *KillList) save() error { dir := filepath.Dir(kl.filepath) tmp, err := os.CreateTemp(dir, ".killist-*.tmp") if err != nil { return err } tmpName := tmp.Name() w := bufio.NewWriter(tmp) for t := range kl.titles { if _, err := w.WriteString(t + "\n"); err != nil { tmp.Close() os.Remove(tmpName) return err } } if err := w.Flush(); err != nil { tmp.Close() os.Remove(tmpName) return err } if err := tmp.Close(); err != nil { os.Remove(tmpName) return err } return os.Rename(tmpName, kl.filepath) }