Makefile: add pickrand
[cmccabe-bin] / superrip2.go
1 package main
2
3 import (
4         "bufio"
5         "flag"
6         "fmt"
7         "io/ioutil"
8         "math/rand"
9         "os"
10         "os/exec"
11         "path/filepath"
12         "strings"
13         "time"
14 )
15
16 func usage(retval int) {
17         fmt.Printf("superrip2: rips a music CD to directories of mp3s and flacs.\n")
18         os.Exit(retval)
19 }
20
21 func main() {
22         var cdRomDevPath string
23         var titleFilePath string
24         var wavDirectory string
25
26         rand.Seed(time.Now().UnixNano())
27         flag.StringVar(&cdRomDevPath, "d", "", "If you want to run cdparanoia, the path to the cdrom device.")
28         flag.StringVar(&titleFilePath, "t", "", "The path to the track title files.")
29         flag.StringVar(&wavDirectory, "w", "", "If the files have already been ripped into a directory " +
30                 "and you want to start from there, the wav directory to look at.")
31         flag.Usage = func() {
32                 fmt.Printf("superrip2: rips a music CD to directories of mp3s and flacs.\n")
33                 fmt.Printf("\n")
34                 fmt.Printf("This program relies on a track title file which contains one line per track.\n")
35                 fmt.Printf("For example, the lines for a classical music CD with 4 tracks might be:\n")
36                 fmt.Printf("\n")
37                 fmt.Printf("Mozart - K412 Horn concerto in D Major/01 - Allegro\n")
38                 fmt.Printf("Mozart - K412 Horn concerto in D Major/02 - Rondo (allegro)\n")
39                 fmt.Printf("Mozart - K417 Horn concerto in E flat major/01 - Allegro maestoso\n")
40                 fmt.Printf("Mozart - K417 Horn concerto in E flat major/02 - Andante\n")
41                 fmt.Printf("\n")
42                 fmt.Printf("The program will then create the K412 and K417 directories, as well\n")
43                 fmt.Printf("as flac versions of those directories.\n")
44                 fmt.Printf("\n")
45                 flag.PrintDefaults()
46         }
47         flag.Parse()
48         if titleFilePath == "" {
49                 fmt.Printf("error: you must supply a title file path with -f. Pass -h for help.\n")
50                 os.Exit(1)
51         }
52         if wavDirectory == "" && cdRomDevPath == "" {
53                 fmt.Printf("error: you must supply either a wav directory path or a cdrom device path. " +
54                         "Pass -h for help.\n")
55                 os.Exit(1)
56         }
57         tl, err := loadTrackList(titleFilePath)
58         if err != nil {
59                 fmt.Printf("error loading tracklist: %s\n", err.Error())
60                 os.Exit(1)
61         }
62         var wd *WavDirectory
63         var wavDirectoryToRemove string
64         if wavDirectory != "" {
65                 wd, err = loadWavDir(wavDirectory)
66                 if err != nil {
67                         fmt.Printf("error loading wav directory: %s\n", err.Error())
68                         os.Exit(1)
69                 }
70         } else {
71                 wd, err = runCdParanoia(cdRomDevPath)
72                 if err != nil {
73                         fmt.Printf("error running cdparanoia: %s\n", err.Error())
74                         os.Exit(1)
75                 }
76                 wavDirectoryToRemove = wd.basePath
77         }
78         err = wd.Process(tl)
79         if err != nil {
80                 fmt.Printf("error processing wav directory: %s\n", err.Error())
81                 os.Exit(1)
82         }
83         if wavDirectoryToRemove != "" {
84                 err = os.RemoveAll(wavDirectoryToRemove)
85                 if err != nil {
86                         fmt.Printf("failed to remove %s: %s\n", wavDirectoryToRemove, err.Error())
87                         os.Exit(1)
88                 }
89         }
90         os.Exit(0)
91 }
92
93 type Track struct {
94         album string
95         title string
96 }
97
98 func trackFromLine(line string) (*Track, error) {
99         sep := strings.IndexRune(line, '/')
100         if sep < 0 {
101                 return nil, fmt.Errorf("Unable to find slash separator in track name %s", line)
102         }
103         t := &Track{
104                 album: line[0:sep],
105                 title: line[sep+1:len(line)],
106         }
107         if strings.HasSuffix(t.title, ".mp3") {
108                 return nil, fmt.Errorf("Track title should not end in .mp3 for %s", line)
109         }
110         if strings.IndexRune(t.title, '/') != -1 {
111                 return nil, fmt.Errorf("Only album and title are allowed, not multiple directory layers, for %s", line)
112         }
113         return t, nil
114 }
115
116 func (t *Track) String() string {
117         return fmt.Sprintf("Track(album=%s, title=%s)", t.album, t.title)
118 }
119
120 type TrackList []*Track
121
122 func loadTrackList(p string) (TrackList, error) {
123         var tl []*Track
124
125         f, err := os.Open(p)
126         if err != nil {
127                 return tl, fmt.Errorf("Unable to open tracklist file %s: %s", p, err.Error())
128         }
129         defer f.Close()
130         scanner := bufio.NewScanner(f)
131         lineNo := 1
132         for scanner.Scan() {
133                 t, err := trackFromLine(scanner.Text())
134                 if err != nil {
135                         return tl, fmt.Errorf("Error parsing line %d of %s: %s", lineNo, p, err.Error())
136                 }
137                 tl = append(tl, t)
138                 lineNo = lineNo + 1
139         }
140         return tl, nil
141 }
142
143 type WavDirectory struct {
144         basePath string
145         fileNames []string
146 }
147
148 func loadWavDir(p string) (*WavDirectory, error) {
149         infos, err := ioutil.ReadDir(p)
150         if err != nil {
151                 return nil, fmt.Errorf("ioutil.ReadDir failed on wave file directory %s: %s", p, err.Error())
152         }
153         fileNames := make([]string, len(infos))
154         for i := range(infos) {
155                 if (infos[i].IsDir()) {
156                         return nil, fmt.Errorf("wav directory %s unexpectedly contained another directory " +
157                                 "named %s", p, infos[i].Name())
158                 }
159                 fileNames[i] = infos[i].Name()
160         }
161         return &WavDirectory{
162                 basePath: p,
163                 fileNames: fileNames,
164         }, nil
165 }
166
167 func runCdParanoia(cdRomDevPath string) (*WavDirectory, error) {
168         tempDir := filepath.Join(".", fmt.Sprintf("cdparanoiaTemp%d%d", rand.Int(), rand.Int()))
169         err := os.Mkdir(tempDir, 0755)
170         if err != nil {
171                 return nil, fmt.Errorf("Failed to create directory %s: %s", tempDir, err.Error())
172         }
173         cmd := exec.Command("cdparanoia", "-B", "-d", cdRomDevPath)
174         cmd.Stdout = os.Stdout
175         cmd.Stderr = os.Stderr
176         cmd.Dir = tempDir
177         err = cmd.Run()
178         if err != nil {
179                 return nil, fmt.Errorf("Failed to run cdparanoia: %s", err.Error())
180         }
181         return loadWavDir(tempDir)
182 }
183
184 func mkdirIfNeeded(p string, prevDirs map[string]bool, what string) error {
185         if prevDirs[p] == true {
186                 return nil
187         }
188         prevDirs[p] = true
189         err := os.MkdirAll(p, 0755)
190         if err != nil {
191                 return fmt.Errorf("Unable to mkdir %s %s: %s", what, p, err.Error())
192         }
193         return nil
194 }
195
196 func (wd *WavDirectory) Process(tl TrackList) error {
197         if len(tl) != len(wd.fileNames) {
198                 return fmt.Errorf("Found %d track(s) in track list but %d in wav directory",
199                         len(tl), len(wd.fileNames))
200         }
201         prevDirs := make(map[string]bool)
202         for i := range(tl) {
203                 t := tl[i]
204                 err := mkdirIfNeeded(filepath.Join(".", t.album), prevDirs, "mp3 directory")
205                 if err != nil {
206                         return err
207                 }
208                 err = mkdirIfNeeded(filepath.Join(".", t.album) + " [LL]", prevDirs, "flac directory")
209                 if err != nil {
210                         return err
211                 }
212         }
213         for i := range(tl) {
214                 t := tl[i]
215                 p := filepath.Join(wd.basePath, wd.fileNames[i])
216                 mp3Path, err := generateMp3(p)
217                 newMp3Path := filepath.Join(".", t.album, t.title) + ".mp3"
218                 if err != nil {
219                         return fmt.Errorf("Error generating mp3 file for %s: %s", p, err.Error())
220                 }
221                 err = os.Rename(mp3Path, newMp3Path)
222                 if err != nil {
223                         return fmt.Errorf("Unable to rename: %s", err.Error())
224                 }
225                 flacPath, err := generateFlac(p)
226                 if err != nil {
227                         return fmt.Errorf("Error generating flac file for %s: %s", p, err.Error())
228                 }
229                 newFlacPath := filepath.Join(".", t.album + " [LL]", t.title) + ".flac"
230                 err = os.Rename(flacPath, newFlacPath)
231                 if err != nil {
232                         return fmt.Errorf("Unable to rename: %s", err.Error())
233                 }
234         }
235         return nil
236 }
237
238 func generateMp3(p string) (string, error) {
239         cmd := exec.Command("lame", "-q", "2", "-b", "256", p, p + ".mp3")
240         err := cmd.Run()
241         if err != nil {
242                 return "", fmt.Errorf("Failed to run lame on %s: %s", p, err.Error())
243         }
244         return p + ".mp3", nil
245 }
246
247 func generateFlac(p string) (string, error) {
248         cmd := exec.Command("flac", p, "-o", p + ".flac")
249         err := cmd.Run()
250         if err != nil {
251                 return "", fmt.Errorf("Failed to run flac on %s: %s", p, err.Error())
252         }
253         return p + ".flac", nil
254 }
255
256 func (wd *WavDirectory) String() string {
257         return fmt.Sprintf("WavDirectory(basePath=%s, fileNames=%s)", wd.basePath, wd.fileNames)
258 }
259