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