package main import ( "context" "git.m3d.pw/majomi/mmm/internal/db" "github.com/dhowden/tag" "github.com/jackc/pgx/v5/pgxpool" "io/fs" "log" "log/slog" "os" "path/filepath" "sync" "time" ) type app struct{} type FileScanner struct { path string ch chan string stop chan struct{} q db.Querier d time.Duration } type MusicFile struct { path string tags tag.Metadata } func main() { musicPath, ok := os.LookupEnv("MMM_DIR") if !ok { log.Fatal("MMM_DIR not set") } dsn, ok := os.LookupEnv("MMM_DB") if !ok { log.Fatal("MMM_DB not set") } pool, err := pgxpool.New(context.Background(), dsn) if err != nil { log.Fatal(err) } q := db.New(pool) scanner := NewFileScanner(musicPath, q, time.Hour) scanner.Scan() } func NewFileScanner(path string, q db.Querier, d time.Duration) *FileScanner { return &FileScanner{path: path, ch: make(chan string), stop: make(chan struct{}), q: q, d: d} } func (f *FileScanner) Scan() { go f.walkDir() for path := range f.ch { go func() { exists, err := f.q.Exists(context.Background(), path) if err != nil { log.Fatal(err) } if exists { return } file, err := os.Open(path) if err != nil { log.Fatal(err) } tags, err := tag.ReadFrom(file) if err != nil { log.Fatal(err) } m := MusicFile{ path: path, tags: tags, } log.Printf("Adding to DB: %s: %+v", m.path, m.tags.Title()) track, err := f.q.AddTrack(context.Background(), db.AddTrackParams{ Path: m.path, AlbumArtist: m.tags.AlbumArtist(), Title: m.tags.Title(), Album: m.tags.Album(), Year: int32(m.tags.Year()), Artist: m.tags.Artist(), Genre: m.tags.Genre(), Lyrics: m.tags.Lyrics(), Composer: m.tags.Composer(), }) if err != nil { log.Fatal(err) } log.Println("Added ", track.Title) }() } } func (f *FileScanner) walkDir() { defer close(f.ch) ticker := time.NewTicker(f.d) // Create walk function, so we can use it right when we start and don't need to wait for the ticker to tick walk := func() { filepath.WalkDir(f.path, func(path string, d fs.DirEntry, err error) error { if d.IsDir() || filepath.Ext(path) != ".flac" { slog.Debug("skipping directory: ", path) return nil } f.ch <- path return err }) } go walk() for { select { case <-ticker.C: go walk() } } } func (f *FileScanner) addToDB(path string) { file, err := os.Open(path) if err != nil { log.Fatal(err) } tags, err := tag.ReadFrom(file) if err != nil { log.Fatal(err) } log.Println("Adding to DB: ", tags.Title()) } func index(path string, wg *sync.WaitGroup) <-chan string { fileChan := make(chan string) go func(wg *sync.WaitGroup) { filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error { if d.IsDir() || filepath.Ext(path) != ".flac" { slog.Debug("skipping directory: ", path) return nil } fileChan <- path wg.Add(1) return err }) close(fileChan) }(wg) return fileChan } func readTag(path string, wg *sync.WaitGroup) { wg.Add(1) defer wg.Done() slog.Debug("reading tag", "path", path) f, err := os.Open(path) if err != nil { slog.Warn("failed to open file: ", path) return } defer f.Close() tags, err := tag.ReadFrom(f) if err != nil { slog.Warn("failed to parse file", "path", path) return } slog.Debug("tags", "title", tags.Title()) }