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) { songs := []ScannedSong{} if _, err := exec.LookPath("ffprobe"); err != nil { return songs, fmt.Errorf("ffprobe not found in PATH: %v", err) } 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 } if song, err := processAudioFile(path); err != nil { // Log error but continue scanning other files return nil } else { songs = append(songs, song) } return nil }) return songs, err } 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) }