Makefile: add pickrand
[cmccabe-bin] / audiobooker.go
1 package main
2
3 import (
4         "os/exec"
5         "bufio"
6         "io"
7         "path/filepath"
8         "io/ioutil"
9         "fmt"
10         "math"
11         "os"
12         "sort"
13         "path"
14         "strings"
15         "strconv"
16 )
17
18 const MP3_SUFFIX = ".mp3"
19
20 const TEMP_FILE_PREFIX = "audiobooker_"
21
22 func checkInPath(inPath string) error {
23         if !strings.HasSuffix(inPath, MP3_SUFFIX) {
24                 return fmt.Errorf("%s: not an mp3 file.\n", inPath)
25         }
26         _, err := os.Stat(inPath); if err != nil {
27                 return fmt.Errorf("%s: %s", inPath, err)
28         }
29         return nil
30 }
31
32 func checkInPaths(inPaths []string) error {
33         for i := range inPaths {
34                 err := checkInPath(inPaths[i]); if err != nil {
35                         return err
36                 }
37         }
38         return nil
39 }
40
41 func promptInPaths(inPaths []string) error {
42         fmt.Printf("FILES TO PROCESS:\n")
43         for i := range inPaths {
44                 fmt.Printf("%s\n", inPaths[i])
45         }
46         fmt.Printf("PROCESS FILES? [Y/N]\n")
47         scanner := bufio.NewScanner(os.Stdin)
48         if !scanner.Scan() {
49                 return fmt.Errorf("EOF reached on stdin.")
50         }
51         if scanner.Err() != nil {
52                 return fmt.Errorf("Error reading stdin: %s.", scanner.Err().Error())
53         }
54         text := scanner.Text()
55         if text == "Y" || text == "y" {
56                 return nil
57         }
58         if text == "N" || text == "n" {
59                 return fmt.Errorf("User declined.")
60         }
61         return fmt.Errorf("Unknown input: %s.", text)
62 }
63
64 type OutDir struct {
65         outPath string
66         nextIndex int
67 }
68
69 func checkDirectoryIsEmpty(dirPath string) error {
70         d, err := os.Open(dirPath); if err != nil {
71                 return fmt.Errorf("Unable to open directory %s: %s", dirPath, err.Error())
72         }
73         defer d.Close()
74         names, err := d.Readdirnames(3); if err != nil && err != io.EOF {
75                 return fmt.Errorf("Unable to readdir %s: %s", dirPath, err.Error())
76         }
77         for i := range names {
78                 if names[i] != "." || names[i] != ".." {
79                         return fmt.Errorf("Directory %s is not empty: contained %s", dirPath, names[i])
80                 }
81         }
82         return nil
83 }
84
85 func createOutDir(outPath string) (*OutDir, error) {
86         err := checkDirectoryIsEmpty(outPath); if err != nil {
87                 return nil, fmt.Errorf("Invalid output directory: %s", err.Error())
88         }
89         return &OutDir {
90                 outPath: outPath,
91                 nextIndex: 0,
92         }, nil
93 }
94
95 func copyFileContents(dst, src string) error {
96         in, err := os.Open(src)
97         if err != nil {
98                 return fmt.Errorf("Unable to open %s: %s", src, err.Error())
99         }
100         defer in.Close()
101         out, err := os.Create(dst)
102         if err != nil {
103                 return fmt.Errorf("Unable to create %s: %s", dst, err.Error())
104         }
105         defer out.Close()
106         if _, err = io.Copy(out, in); err != nil {
107                 return fmt.Errorf("Error copying data from %s to %s: %s",
108                         src, dst, err.Error())
109         }
110         err = out.Sync()
111         if err != nil {
112                 return fmt.Errorf("Failed to write %s: %s", dst, err.Error())
113         }
114         return err
115 }
116
117 func (out *OutDir) CopyFile(src string) error {
118         dst := path.Join(out.outPath,
119                 fmt.Sprintf("%010d%s", out.nextIndex, filepath.Ext(src)))
120         err := copyFileContents(dst, src)
121         out.nextIndex++
122         return err
123 }
124
125 func (out *OutDir) GetNumDigitsNeeded() int {
126         i := math.Log10(float64(out.nextIndex))
127         j := math.Floor(i)
128         if j < i {
129                 return 1 + int(j)
130         } else {
131                 return int(j)
132         }
133 }
134
135 func (out *OutDir) RenameOutputFiles() error {
136         numDigitsNeeded := out.GetNumDigitsNeeded()
137         fs := "%0" + strconv.Itoa(numDigitsNeeded) + "d" + MP3_SUFFIX
138         d, err := os.Open(out.outPath); if err != nil {
139                 return fmt.Errorf("Unable to open directory %s: %s", out.outPath, err.Error())
140         }
141         defer d.Close()
142         names, err := d.Readdirnames(0); if err != nil && err != io.EOF {
143                 return fmt.Errorf("Unable to readdir %s: %s", out.outPath, err.Error())
144         }
145         for i := range names {
146                 index := -1
147                 _, err = fmt.Sscanf(names[i], "%d", &index); if err != nil {
148                         return fmt.Errorf("Unable to parse file name %s", names[i])
149                 }
150                 newName := fmt.Sprintf(fs, index)
151                 src := path.Join(out.outPath, names[i])
152                 dst := path.Join(out.outPath, newName)
153                 err = os.Rename(src, dst)
154                 if err != nil {
155                         return fmt.Errorf("Failed to rename %s to %s: %s", src, dst, err.Error())
156                 }
157         }
158         return nil
159 }
160
161 type InContext struct {
162         inPath string // The original input file path.
163         tempDir string // The path to the temporary directory.
164         srcPath string // The path to the copy of the source file.
165 }
166
167 func createInContext(inPath string) (*InContext, error) {
168         tempDir, err := ioutil.TempDir(os.TempDir(), TEMP_FILE_PREFIX); if err != nil {
169                 return nil, fmt.Errorf("Error creating temp dir: %s", err.Error())
170         }
171         in := &InContext {
172                 inPath: inPath,
173                 tempDir: tempDir,
174                 srcPath: path.Join(tempDir, "src"),
175         }
176         err = copyFileContents(in.srcPath, inPath)
177         if err != nil {
178                 in.Cleanup()
179                 return nil, err
180         }
181         return in, nil
182 }
183
184 func (in *InContext) Split() error {
185         command := exec.Command("mp3splt", "-t", "5.0", "-n", "-f",
186                 "-o", TEMP_FILE_PREFIX + "@n", in.srcPath)
187         err := command.Run()
188         if err != nil {
189                 return fmt.Errorf("mp3splt failed on %s: %s", in.srcPath, err.Error())
190         }
191         return nil
192 }
193
194 func (in *InContext) GetPaths() ([]string, error) {
195         d, err := os.Open(in.tempDir); if err != nil {
196                 return nil, fmt.Errorf("Unable to open directory %s: %s", in.tempDir, err.Error())
197         }
198         defer d.Close()
199         names, err := d.Readdirnames(0); if err != nil && err != io.EOF {
200                 return nil, fmt.Errorf("Unable to readdir %s: %s", in.tempDir, err.Error())
201         }
202         inPathBase := filepath.Base(in.srcPath)
203         paths := make([]string, 0, len(names))
204         for i := range names {
205                 if names[i] != "." && names[i] != ".." && names[i] != inPathBase {
206                         paths = append(paths, path.Join(in.tempDir, names[i]))
207                 }
208         }
209         sort.Strings(paths)
210         return paths, nil
211 }
212
213 func (in *InContext) Cleanup() error {
214         return os.RemoveAll(in.tempDir)
215 }
216
217 func printUsage() {
218         fmt.Printf("audiobooker: splits audiobook mp3 files into 5 minute segments.\n")
219         fmt.Printf("usage: audiobooker [mp3 paths...]\n")
220         fmt.Printf("\n")
221         fmt.Printf("Note: the split mp3s will be stored in the current working directory.\n")
222 }
223
224 func main() {
225         if len(os.Args) < 2 {
226                 printUsage()
227                 os.Exit(0)
228         }
229         inPaths := os.Args[1:]
230         err := checkInPaths(inPaths); if err != nil {
231                 fmt.Printf("Error: %s\n", err.Error())
232                 os.Exit(1)
233         }
234         err = promptInPaths(inPaths); if err != nil {
235                 fmt.Printf("%s\n", err.Error())
236                 os.Exit(1)
237         }
238         outDir, err := createOutDir("."); if err != nil {
239                 fmt.Printf("%s\n", err.Error())
240                 os.Exit(1)
241         }
242         var totalFiles int64
243         for i := range inPaths {
244                 in, err := createInContext(inPaths[i]); if err != nil {
245                         fmt.Printf("Error processing %s: %s", inPaths[i], err.Error())
246                         os.Exit(1)
247                 }
248                 defer in.Cleanup()
249                 err = in.Split(); if err != nil {
250                         fmt.Printf("%s\n", err.Error())
251                         os.Exit(1)
252                 }
253                 paths, err := in.GetPaths(); if err != nil {
254                         fmt.Printf("Error processing %s: %s", inPaths[i], err.Error())
255                         os.Exit(1)
256                 }
257                 for j := range paths {
258                         err = outDir.CopyFile(paths[j]); if err != nil {
259                                 fmt.Printf("Error processing %s: %s", inPaths[i], err.Error())
260                                 os.Exit(1)
261                         }
262                 }
263                 fmt.Printf("Split %s into %d file(s)\n", inPaths[i], len(paths))
264                 totalFiles += int64(len(paths))
265         }
266         err = outDir.RenameOutputFiles()
267         if err != nil {
268                 fmt.Printf("Error renaming output files in %s: %s", outDir.outPath, err.Error())
269                 os.Exit(1)
270         }
271         fmt.Printf("Processed %d total file(s)\n", totalFiles)
272 }