122 lines
2.8 KiB
Go
122 lines
2.8 KiB
Go
// Package scan walks a directory tree and yields the audio files under it.
|
|||
|
|
// It replaces the archived code.aneur.in/go/sqan with a plain
|
||
|
|
// filepath.WalkDir; zampler only ever filtered by extension.
|
||
|
|
package scan
|
||
|
|
|
||
|
|
import (
|
||
|
|
"io/fs"
|
||
|
|
"iter"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"slices"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// File describes one scanned file. Paths mirror sqan: RootDir and
|
||
|
|
// AbsolutePath are absolute, RelativePath is rooted with a leading slash
|
||
|
|
// ("/album/track.mp3"), RelativeDir is its directory ("/album", or "/" for
|
||
|
|
// a file directly under the root).
|
||
|
|
type File struct {
|
||
|
|
RootDir string
|
||
|
|
AbsolutePath string
|
||
|
|
RelativeDir string
|
||
|
|
RelativePath string
|
||
|
|
Filename string
|
||
|
|
Extension string
|
||
|
|
Size int64
|
||
|
|
Modified time.Time
|
||
|
|
}
|
||
|
|
|
||
|
|
// Scanner walks a single root directory.
|
||
|
|
type Scanner struct {
|
||
|
|
root string
|
||
|
|
extensions []string
|
||
|
|
onError func(error)
|
||
|
|
}
|
||
|
|
|
||
|
|
// New creates a Scanner for root. A relative root is resolved against the
|
||
|
|
// working directory.
|
||
|
|
func New(root string) *Scanner {
|
||
|
|
if !filepath.IsAbs(root) {
|
||
|
|
if cwd, err := os.Getwd(); err == nil {
|
||
|
|
root = filepath.Join(cwd, root)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return &Scanner{root: root, onError: func(error) {}}
|
||
|
|
}
|
||
|
|
|
||
|
|
// WithExtensions restricts the scan to files with one of the given
|
||
|
|
// extensions (without the dot), compared case-insensitively.
|
||
|
|
func (s *Scanner) WithExtensions(exts ...string) *Scanner {
|
||
|
|
for _, ext := range exts {
|
||
|
|
s.extensions = append(s.extensions, strings.ToLower(ext))
|
||
|
|
}
|
||
|
|
return s
|
||
|
|
}
|
||
|
|
|
||
|
|
// OnError registers a callback for errors hit while walking. Unset, they
|
||
|
|
// are ignored and the walk continues.
|
||
|
|
func (s *Scanner) OnError(fn func(error)) *Scanner {
|
||
|
|
s.onError = fn
|
||
|
|
return s
|
||
|
|
}
|
||
|
|
|
||
|
|
// Scan walks the tree, yielding one *File per matching file. Stopping the
|
||
|
|
// range stops the walk.
|
||
|
|
func (s *Scanner) Scan() iter.Seq[*File] {
|
||
|
|
return func(yield func(*File) bool) {
|
||
|
|
filepath.WalkDir(s.root, func(path string, d fs.DirEntry, err error) error {
|
||
|
|
if err != nil {
|
||
|
|
s.onError(err)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
if d.IsDir() {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
name := d.Name()
|
||
|
|
ext := ""
|
||
|
|
if i := strings.LastIndexByte(name, '.'); i > -1 {
|
||
|
|
ext = strings.ToLower(name[i+1:])
|
||
|
|
}
|
||
|
|
if len(s.extensions) > 0 && !slices.Contains(s.extensions, ext) {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
rel, relErr := filepath.Rel(s.root, path)
|
||
|
|
if relErr != nil {
|
||
|
|
s.onError(relErr)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
rel = "/" + filepath.ToSlash(rel)
|
||
|
|
|
||
|
|
relDir := "/"
|
||
|
|
if i := strings.LastIndexByte(rel, '/'); i > 0 {
|
||
|
|
relDir = rel[:i]
|
||
|
|
}
|
||
|
|
|
||
|
|
info, infoErr := d.Info()
|
||
|
|
if infoErr != nil {
|
||
|
|
s.onError(infoErr)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
file := &File{
|
||
|
|
RootDir: s.root,
|
||
|
|
AbsolutePath: path,
|
||
|
|
RelativeDir: relDir,
|
||
|
|
RelativePath: rel,
|
||
|
|
Filename: name,
|
||
|
|
Extension: ext,
|
||
|
|
Size: info.Size(),
|
||
|
|
Modified: info.ModTime(),
|
||
|
|
}
|
||
|
|
if !yield(file) {
|
||
|
|
return filepath.SkipAll
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|