package main import ( "fmt" "io/fs" "io/ioutil" "os" "path" "sort" "strings" "time" "github.com/dhowden/tag" ) type metadata struct { Title string Artist string Album string Track int Length time.Duration } func readMetadata(f *os.File) *metadata { var Metadata metadata m, err := tag.ReadFrom(f) if err != nil || m.Title() == "" { Metadata.Title = strings.TrimSpace(path.Base(f.Name())) } else { Metadata.Title = strings.TrimSpace(m.Title()) Metadata.Artist = strings.TrimSpace(m.Artist()) Metadata.Album = strings.TrimSpace(m.Album()) Metadata.Track, _ = m.Track() /* TODO Too slow d := mp3.NewDecoder(f) var frame mp3.Frame var skipped int for { err = d.Decode(&frame, &skipped) if err != nil { break } metadata.Length += frame.Duration() } */ } return &Metadata } type libraryEntry struct { Name string IsDir bool Mode fs.FileMode Path string RealPath string Metadata *metadata } func (e *libraryEntry) String() string { if e.Metadata.Title != "" { if e.Metadata.Artist != "" { return e.Metadata.Artist + " - " + e.Metadata.Title } return e.Metadata.Title } return strings.TrimSpace(e.Name) } func scanFolder(scanPath string) []*libraryEntry { files, err := ioutil.ReadDir(scanPath) if err != nil { panic(fmt.Sprintf("failed to scan %s: %s", scanPath, err)) } var entries []*libraryEntry for _, fileInfo := range files { p := path.Join(scanPath, fileInfo.Name()) var r string if fileInfo.Mode()&os.ModeSymlink != 0 { r, err = os.Readlink(p) if err != nil { continue } if !path.IsAbs(r) { r = path.Join(scanPath, r) } } else { r = p } b := path.Base(p) if fileInfo.IsDir() || fileInfo.Mode()&os.ModeSymlink != 0 { if b != "" && (b[0] != '.' || showHiddenFolders) { entries = append(entries, &libraryEntry{Name: fileInfo.Name(), IsDir: fileInfo.IsDir(), Mode: fileInfo.Mode(), Path: p, RealPath: r, Metadata: &metadata{Title: strings.TrimSpace(fileInfo.Name())}}) } continue } else if !supportedFormat(b) { continue } f, err := os.Open(r) if err != nil { continue } Metadata := readMetadata(f) f.Close() entries = append(entries, &libraryEntry{Name: fileInfo.Name(), IsDir: fileInfo.IsDir(), Mode: fileInfo.Mode(), Path: p, RealPath: r, Metadata: Metadata}) } sort.Slice(entries, func(i, j int) bool { iDir := entries[i].IsDir || entries[i].Mode&os.ModeSymlink != 0 jDir := entries[j].IsDir || entries[j].Mode&os.ModeSymlink != 0 if iDir != jDir { return iDir } if entries[i].Metadata.Album != "" && strings.ToLower(entries[i].Metadata.Album) == strings.ToLower(entries[j].Metadata.Album) && (entries[i].Metadata.Track > 0 || entries[j].Metadata.Track > 0) { return entries[i].Metadata.Track < entries[j].Metadata.Track } return strings.ToLower(entries[i].Metadata.Album) < strings.ToLower(entries[j].Metadata.Album) && strings.ToLower(entries[i].Name) < strings.ToLower(entries[j].Name) }) return entries } func scanFolderRecursively(path string) []*libraryEntry { var entries []*libraryEntry scanFiles := scanFolder(path) for _, entry := range scanFiles { if !entry.IsDir && entry.Mode&os.ModeSymlink == 0 { continue } entries = append(entries, scanFolderRecursively(entry.RealPath)...) } for _, entry := range scanFiles { if entry.IsDir || entry.Mode&os.ModeSymlink != 0 { continue } entries = append(entries, entry) } return entries }