Add superrip2
authorColin P. Mccabe <colin@cmccabe.xyz>
Mon, 23 Aug 2021 05:37:36 +0000 (22:37 -0700)
committerColin P. Mccabe <colin@cmccabe.xyz>
Mon, 23 Aug 2021 06:09:33 +0000 (23:09 -0700)
.gitignore
superrip2.go [new file with mode: 0644]

index b6a0926..da2ce42 100644 (file)
@@ -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 (file)
index 0000000..6a88d61
--- /dev/null
@@ -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)
+}
+