package main import ( "bufio" "flag" "fmt" "io/ioutil" "math/rand" "os" "os/exec" "path/filepath" "strings" "time" ) func usage(retval int) { fmt.Printf("superrip2: rips a music CD to directories of mp3s and flacs.\n") os.Exit(retval) } func main() { var cdRomDevPath string var titleFilePath string var wavDirectory string rand.Seed(time.Now().UnixNano()) flag.StringVar(&cdRomDevPath, "d", "", "If you want to run cdparanoia, the path to the cdrom device.") flag.StringVar(&titleFilePath, "t", "", "The path to the track title files.") flag.StringVar(&wavDirectory, "w", "", "If the files have already been ripped into a directory " + "and you want to start from there, the wav directory to look at.") flag.Usage = func() { fmt.Printf("superrip2: rips a music CD to directories of mp3s and flacs.\n") fmt.Printf("\n") fmt.Printf("This program relies on a track title file which contains one line per track.\n") fmt.Printf("For example, the lines for a classical music CD with 4 tracks might be:\n") fmt.Printf("\n") fmt.Printf("Mozart - K412 Horn concerto in D Major/01 - Allegro\n") fmt.Printf("Mozart - K412 Horn concerto in D Major/02 - Rondo (allegro)\n") fmt.Printf("Mozart - K417 Horn concerto in E flat major/01 - Allegro maestoso\n") fmt.Printf("Mozart - K417 Horn concerto in E flat major/02 - Andante\n") fmt.Printf("\n") fmt.Printf("The program will then create the K412 and K417 directories, as well\n") fmt.Printf("as flac versions of those directories.\n") fmt.Printf("\n") flag.PrintDefaults() } flag.Parse() if titleFilePath == "" { fmt.Printf("error: you must supply a title file path with -f. Pass -h for help.\n") os.Exit(1) } if wavDirectory == "" && cdRomDevPath == "" { fmt.Printf("error: you must supply either a wav directory path or a cdrom device path. " + "Pass -h for help.\n") os.Exit(1) } tl, err := loadTrackList(titleFilePath) if err != nil { fmt.Printf("error loading tracklist: %s\n", err.Error()) os.Exit(1) } var wd *WavDirectory var wavDirectoryToRemove string if wavDirectory != "" { wd, err = loadWavDir(wavDirectory) if err != nil { fmt.Printf("error loading wav directory: %s\n", err.Error()) os.Exit(1) } } else { wd, err = runCdParanoia(cdRomDevPath) if err != nil { fmt.Printf("error running cdparanoia: %s\n", err.Error()) os.Exit(1) } wavDirectoryToRemove = wd.basePath } err = wd.Process(tl) if err != nil { fmt.Printf("error processing wav directory: %s\n", err.Error()) os.Exit(1) } if wavDirectoryToRemove != "" { err = os.RemoveAll(wavDirectoryToRemove) if err != nil { fmt.Printf("failed to remove %s: %s\n", wavDirectoryToRemove, err.Error()) os.Exit(1) } } os.Exit(0) } type Track struct { album string title string } func trackFromLine(line string) (*Track, error) { sep := strings.IndexRune(line, '/') if sep < 0 { return nil, fmt.Errorf("Unable to find slash separator in track name %s", line) } t := &Track{ album: line[0:sep], title: line[sep+1:len(line)], } if strings.HasSuffix(t.title, ".mp3") { return nil, fmt.Errorf("Track title should not end in .mp3 for %s", line) } if strings.IndexRune(t.title, '/') != -1 { return nil, fmt.Errorf("Only album and title are allowed, not multiple directory layers, for %s", line) } return t, nil } func (t *Track) String() string { return fmt.Sprintf("Track(album=%s, title=%s)", t.album, t.title) } type TrackList []*Track func loadTrackList(p string) (TrackList, error) { var tl []*Track f, err := os.Open(p) if err != nil { return tl, fmt.Errorf("Unable to open tracklist file %s: %s", p, err.Error()) } defer f.Close() scanner := bufio.NewScanner(f) lineNo := 1 for scanner.Scan() { t, err := trackFromLine(scanner.Text()) if err != nil { return tl, fmt.Errorf("Error parsing line %d of %s: %s", lineNo, p, err.Error()) } tl = append(tl, t) lineNo = lineNo + 1 } return tl, nil } type WavDirectory struct { basePath string fileNames []string } func loadWavDir(p string) (*WavDirectory, error) { infos, err := ioutil.ReadDir(p) if err != nil { return nil, fmt.Errorf("ioutil.ReadDir failed on wave file directory %s: %s", p, err.Error()) } fileNames := make([]string, len(infos)) for i := range(infos) { if (infos[i].IsDir()) { return nil, fmt.Errorf("wav directory %s unexpectedly contained another directory " + "named %s", p, infos[i].Name()) } fileNames[i] = infos[i].Name() } return &WavDirectory{ basePath: p, fileNames: fileNames, }, nil } func runCdParanoia(cdRomDevPath string) (*WavDirectory, error) { tempDir := filepath.Join(".", fmt.Sprintf("cdparanoiaTemp%d%d", rand.Int(), rand.Int())) err := os.Mkdir(tempDir, 0755) if err != nil { return nil, fmt.Errorf("Failed to create directory %s: %s", tempDir, err.Error()) } cmd := exec.Command("cdparanoia", "-B", "-d", cdRomDevPath) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Dir = tempDir err = cmd.Run() if err != nil { return nil, fmt.Errorf("Failed to run cdparanoia: %s", err.Error()) } return loadWavDir(tempDir) } func mkdirIfNeeded(p string, prevDirs map[string]bool, what string) error { if prevDirs[p] == true { return nil } prevDirs[p] = true err := os.MkdirAll(p, 0755) if err != nil { return fmt.Errorf("Unable to mkdir %s %s: %s", what, p, err.Error()) } return nil } func (wd *WavDirectory) Process(tl TrackList) error { if len(tl) != len(wd.fileNames) { return fmt.Errorf("Found %d track(s) in track list but %d in wav directory", len(tl), len(wd.fileNames)) } prevDirs := make(map[string]bool) for i := range(tl) { t := tl[i] err := mkdirIfNeeded(filepath.Join(".", t.album), prevDirs, "mp3 directory") if err != nil { return err } err = mkdirIfNeeded(filepath.Join(".", t.album) + " [LL]", prevDirs, "flac directory") if err != nil { return err } } for i := range(tl) { t := tl[i] p := filepath.Join(wd.basePath, wd.fileNames[i]) mp3Path, err := generateMp3(p) newMp3Path := filepath.Join(".", t.album, t.title) + ".mp3" if err != nil { return fmt.Errorf("Error generating mp3 file for %s: %s", p, err.Error()) } err = os.Rename(mp3Path, newMp3Path) if err != nil { return fmt.Errorf("Unable to rename: %s", err.Error()) } flacPath, err := generateFlac(p) if err != nil { return fmt.Errorf("Error generating flac file for %s: %s", p, err.Error()) } newFlacPath := filepath.Join(".", t.album + " [LL]", t.title) + ".flac" err = os.Rename(flacPath, newFlacPath) if err != nil { return fmt.Errorf("Unable to rename: %s", err.Error()) } } return nil } func generateMp3(p string) (string, error) { cmd := exec.Command("lame", "-q", "2", "-b", "256", p, p + ".mp3") err := cmd.Run() if err != nil { return "", fmt.Errorf("Failed to run lame on %s: %s", p, err.Error()) } return p + ".mp3", nil } func generateFlac(p string) (string, error) { cmd := exec.Command("flac", p, "-o", p + ".flac") err := cmd.Run() if err != nil { return "", fmt.Errorf("Failed to run flac on %s: %s", p, err.Error()) } return p + ".flac", nil } func (wd *WavDirectory) String() string { return fmt.Sprintf("WavDirectory(basePath=%s, fileNames=%s)", wd.basePath, wd.fileNames) }