package scanner import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "slices" "strconv" "strings" ) type ScannedSong struct { Title string Artist string Album string Duration int Path string } // ffprobeFormat holds the JSON output from ffprobe format section type ffprobeFormat struct { Tags map[string]string `json:"tags"` Duration string `json:"duration"` } // ffprobeOutput holds the full ffprobe JSON output type ffprobeOutput struct { Format ffprobeFormat `json:"format"` } func ScanDirectory(dirPath string) ([]ScannedSong, error) { paths, err := ListAudioFiles(dirPath) if err != nil { return nil, err } songs := make([]ScannedSong, 0, len(paths)) for _, path := range paths { song, err := ProbeAudioFile(path) if err != nil { continue } songs = append(songs, song) } return songs, nil } func ListAudioFiles(dirPath string) ([]string, error) { paths := []string{} err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } if !isAudioFile(path) { return nil } paths = append(paths, path) return nil }) return paths, err } func ProbeAudioFile(filePath string) (ScannedSong, error) { if _, err := exec.LookPath("ffprobe"); err != nil { return ScannedSong{}, fmt.Errorf("ffprobe not found in PATH: %v", err) } return processAudioFile(filePath) } func processAudioFile(filePath string) (ScannedSong, error) { cmd := exec.Command("ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", filePath) output, err := cmd.Output() if err != nil { return ScannedSong{}, fmt.Errorf("ffprobe failed for %s: %v", filePath, err) } var probeOutput ffprobeOutput if err := json.Unmarshal(output, &probeOutput); err != nil { return ScannedSong{}, fmt.Errorf("failed to parse ffprobe output for %s: %v", filePath, err) } tags := probeOutput.Format.Tags if tags == nil { tags = make(map[string]string) } title := tags["title"] if title == "" { title = filepath.Base(filePath) } artist := tags["artist"] if artist == "" { artist = "Unknown Artist" } album := tags["album"] if album == "" { album = "Unknown Album" } duration := 0 if probeOutput.Format.Duration != "" { if d, err := strconv.ParseFloat(probeOutput.Format.Duration, 64); err == nil { duration = int(d) } } return ScannedSong{ Title: title, Artist: artist, Album: album, Duration: duration, Path: filePath, }, nil } func isAudioFile(path string) bool { ext := strings.ToLower(filepath.Ext(path)) audioExtensions := []string{".mp3", ".flac", ".m4a", ".wav", ".ogg"} return slices.Contains(audioExtensions, ext) }