From: Colin P. Mccabe Date: Mon, 23 Aug 2021 05:37:36 +0000 (-0700) Subject: Add superrip2 X-Git-Url: http://club.cc.cmu.edu/~cmccabe/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0553471eb013b0bdd4a555c8cfddae2f68d23991;p=cmccabe-bin Add superrip2 --- diff --git a/.gitignore b/.gitignore index b6a0926..da2ce42 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ pickrand random_word recursive_decompress audiobooker +superrip2 # # Normal rules diff --git a/superrip2.go b/superrip2.go new file mode 100644 index 0000000..6a88d61 --- /dev/null +++ b/superrip2.go @@ -0,0 +1,259 @@ +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) +} +