coda/internal/scanner/scanner.go

184 lines
3.3 KiB
Go

package scanner
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"coda/internal/config"
)
type Scanner struct {
musicPath string
mu sync.RWMutex
cache []Album
hasCached bool
}
func New(cfg *config.Config) *Scanner {
return &Scanner{
musicPath: filepath.Clean(cfg.MusicPath),
}
}
type Entry struct {
Name string
RelPath string
HasFlac bool
Dirs int
}
func (s *Scanner) Browse(relPath string) ([]Entry, error) {
resolved, err := s.Resolve(relPath)
if err != nil {
return nil, err
}
fis, err := os.ReadDir(resolved)
if err != nil {
return nil, fmt.Errorf("read dir %s: %w", resolved, err)
}
var entries []Entry
for _, fi := range fis {
if !fi.IsDir() {
continue
}
entryPath := filepath.Join(resolved, fi.Name())
subFis, err := os.ReadDir(entryPath)
if err != nil {
continue
}
e := Entry{
Name: fi.Name(),
RelPath: filepath.Join(relPath, fi.Name()),
}
for _, sub := range subFis {
if sub.IsDir() {
e.Dirs++
}
if !sub.IsDir() && isFlac(sub.Name()) {
e.HasFlac = true
}
}
entries = append(entries, e)
}
sort.Slice(entries, func(i, j int) bool {
return strings.ToLower(entries[i].Name) < strings.ToLower(entries[j].Name)
})
return entries, nil
}
type Album struct {
RelPath string
AbsPath string
Artist string
Album string
FlacCount int
}
func (s *Scanner) Scan() ([]Album, error) {
type flacDir struct {
absPath string
count int
}
flacDirs := map[string]*flacDir{}
err := filepath.WalkDir(s.musicPath, func(path string, d os.DirEntry, err error) error {
if err != nil {
return nil
}
if d.IsDir() {
return nil
}
if !isFlac(d.Name()) {
return nil
}
dir := filepath.Dir(path)
fd, ok := flacDirs[dir]
if !ok {
fd = &flacDir{absPath: dir}
flacDirs[dir] = fd
}
fd.count++
return nil
})
if err != nil {
return nil, fmt.Errorf("walk: %w", err)
}
for dir := range flacDirs {
for parent := filepath.Dir(dir); parent != s.musicPath && parent != "."; parent = filepath.Dir(parent) {
if _, ok := flacDirs[parent]; ok {
delete(flacDirs, dir)
break
}
}
}
var albums []Album
for absPath, fd := range flacDirs {
relPath, _ := filepath.Rel(s.musicPath, absPath)
albumName := filepath.Base(absPath)
artistName := filepath.Base(filepath.Dir(absPath))
albums = append(albums, Album{
RelPath: relPath,
AbsPath: absPath,
Artist: artistName,
Album: albumName,
FlacCount: fd.count,
})
}
sort.Slice(albums, func(i, j int) bool {
return albums[i].RelPath < albums[j].RelPath
})
s.mu.Lock()
s.cache = albums
s.hasCached = true
s.mu.Unlock()
return albums, nil
}
func (s *Scanner) GetCached() ([]Album, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.cache, s.hasCached
}
func (s *Scanner) Resolve(relPath string) (string, error) {
resolved := filepath.Join(s.musicPath, relPath)
resolved = filepath.Clean(resolved)
if !strings.HasPrefix(resolved, s.musicPath) {
return "", fmt.Errorf("path %q escapes music path", relPath)
}
if resolved != s.musicPath && !strings.HasPrefix(resolved, s.musicPath+string(filepath.Separator)) {
return "", fmt.Errorf("path %q escapes music path", relPath)
}
return resolved, nil
}
func isFlac(name string) bool {
ext := strings.ToLower(filepath.Ext(name))
return ext == ".flac"
}