Add audiobooker
authorColin P. Mccabe <colin@cmccabe.xyz>
Wed, 19 Dec 2018 23:01:01 +0000 (15:01 -0800)
committerColin P. Mccabe <colin@cmccabe.xyz>
Wed, 19 Dec 2018 23:01:01 +0000 (15:01 -0800)
.gitignore
audiobooker.go [new file with mode: 0644]

index 9d11a70..74827ca 100644 (file)
@@ -15,6 +15,7 @@ hexconv
 msgpack-translate
 directory_merge
 pickrand
+audiobooker
 
 #
 # Normal rules
diff --git a/audiobooker.go b/audiobooker.go
new file mode 100644 (file)
index 0000000..c330c89
--- /dev/null
@@ -0,0 +1,229 @@
+package main
+
+import (
+       "os/exec"
+       "bufio"
+       "io"
+       "path/filepath"
+       "io/ioutil"
+       "fmt"
+       "os"
+       "sort"
+       "path"
+       "strings"
+)
+
+const MP3_SUFFIX = ".mp3"
+
+const TEMP_FILE_PREFIX = "audiobooker_"
+
+func checkInPath(inPath string) error {
+       if !strings.HasSuffix(inPath, MP3_SUFFIX) {
+               return fmt.Errorf("%s: not an mp3 file.\n", inPath)
+       }
+       _, err := os.Stat(inPath); if err != nil {
+               return fmt.Errorf("%s: %s", inPath, err)
+       }
+       return nil
+}
+
+func checkInPaths(inPaths []string) error {
+       for i := range inPaths {
+               err := checkInPath(inPaths[i]); if err != nil {
+                       return err
+               }
+       }
+       return nil
+}
+
+func promptInPaths(inPaths []string) error {
+       fmt.Printf("FILES TO PROCESS:\n")
+       for i := range inPaths {
+               fmt.Printf("%s\n", inPaths[i])
+       }
+       fmt.Printf("PROCESS FILES? [Y/N]\n")
+       scanner := bufio.NewScanner(os.Stdin)
+       if !scanner.Scan() {
+               return fmt.Errorf("EOF reached on stdin.")
+       }
+       if scanner.Err() != nil {
+               return fmt.Errorf("Error reading stdin: %s.", scanner.Err().Error())
+       }
+       text := scanner.Text()
+       if text == "Y" || text == "y" {
+               return nil
+       }
+       if text == "N" || text == "n" {
+               return fmt.Errorf("User declined.")
+       }
+       return fmt.Errorf("Unknown input: %s.", text)
+}
+
+type OutDir struct {
+       outPath string
+       nextIndex int
+}
+
+func checkDirectoryIsEmpty(dirPath string) error {
+       d, err := os.Open(dirPath); if err != nil {
+               return fmt.Errorf("Unable to open directory %s: %s", dirPath, err.Error())
+       }
+       defer d.Close()
+       names, err := d.Readdirnames(3); if err != nil && err != io.EOF {
+               return fmt.Errorf("Unable to readdir %s: %s", dirPath, err.Error())
+       }
+       for i := range names {
+               if names[i] != "." || names[i] != ".." {
+                       return fmt.Errorf("Directory %s is not empty: contained %s", dirPath, names[i])
+               }
+       }
+       return nil
+}
+
+func createOutDir(outPath string) (*OutDir, error) {
+       err := checkDirectoryIsEmpty(outPath); if err != nil {
+               return nil, fmt.Errorf("Invalid output directory: %s", err.Error())
+       }
+       return &OutDir {
+               outPath: outPath,
+               nextIndex: 0,
+       }, nil
+}
+
+func copyFileContents(dst, src string) error {
+       in, err := os.Open(src)
+       if err != nil {
+               return fmt.Errorf("Unable to open %s: %s", src, err.Error())
+       }
+       defer in.Close()
+       out, err := os.Create(dst)
+       if err != nil {
+               return fmt.Errorf("Unable to create %s: %s", dst, err.Error())
+       }
+       defer out.Close()
+       if _, err = io.Copy(out, in); err != nil {
+               return fmt.Errorf("Error copying data from %s to %s: %s",
+                       src, dst, err.Error())
+       }
+       err = out.Sync()
+       if err != nil {
+               return fmt.Errorf("Failed to write %s: %s", dst, err.Error())
+       }
+       return err
+}
+
+func (out *OutDir) CopyFile(src string) error {
+       dst := path.Join(out.outPath,
+               fmt.Sprintf("%010d%s", out.nextIndex, filepath.Ext(src)))
+       err := copyFileContents(dst, src)
+       out.nextIndex++
+       return err
+}
+
+type InContext struct {
+       inPath string // The original input file path.
+       tempDir string // The path to the temporary directory.
+       srcPath string // The path to the copy of the source file.
+}
+
+func createInContext(inPath string) (*InContext, error) {
+       tempDir, err := ioutil.TempDir(os.TempDir(), TEMP_FILE_PREFIX); if err != nil {
+               return nil, fmt.Errorf("Error creating temp dir: %s", err.Error())
+       }
+       in := &InContext {
+               inPath: inPath,
+               tempDir: tempDir,
+               srcPath: path.Join(tempDir, "src"),
+       }
+       err = copyFileContents(in.srcPath, inPath)
+       if err != nil {
+               in.Cleanup()
+               return nil, err
+       }
+       return in, nil
+}
+
+func (in *InContext) Split() error {
+       command := exec.Command("mp3splt", "-t", "5.0", "-n", "-f",
+               "-o", TEMP_FILE_PREFIX + "@n", in.srcPath)
+       err := command.Run()
+       if err != nil {
+               return fmt.Errorf("mp3splt failed on %s: %s", in.srcPath, err.Error())
+       }
+       return nil
+}
+
+func (in *InContext) GetPaths() ([]string, error) {
+       d, err := os.Open(in.tempDir); if err != nil {
+               return nil, fmt.Errorf("Unable to open directory %s: %s", in.tempDir, err.Error())
+       }
+       defer d.Close()
+       names, err := d.Readdirnames(0); if err != nil && err != io.EOF {
+               return nil, fmt.Errorf("Unable to readdir %s: %s", in.tempDir, err.Error())
+       }
+       inPathBase := filepath.Base(in.srcPath)
+       paths := make([]string, 0, len(names))
+       for i := range names {
+               if names[i] != "." && names[i] != ".." && names[i] != inPathBase {
+                       paths = append(paths, path.Join(in.tempDir, names[i]))
+               }
+       }
+       sort.Strings(paths)
+       return paths, nil
+}
+
+func (in *InContext) Cleanup() error {
+       return os.RemoveAll(in.tempDir)
+}
+
+func printUsage() {
+       fmt.Printf("audiobooker: splits audiobook mp3 files into 5 minute segments.\n")
+       fmt.Printf("usage: audiobooker [mp3 paths...]\n")
+       fmt.Printf("\n")
+       fmt.Printf("Note: the split mp3s will be stored in the current working directory.\n")
+}
+
+func main() {
+       if len(os.Args) < 2 {
+               printUsage()
+               os.Exit(0)
+       }
+       inPaths := os.Args[1:]
+       err := checkInPaths(inPaths); if err != nil {
+               fmt.Printf("Error: %s\n", err.Error())
+               os.Exit(1)
+       }
+       err = promptInPaths(inPaths); if err != nil {
+               fmt.Printf("%s\n", err.Error())
+               os.Exit(1)
+       }
+       outDir, err := createOutDir("."); if err != nil {
+               fmt.Printf("%s\n", err.Error())
+               os.Exit(1)
+       }
+       var totalFiles int64
+       for i := range inPaths {
+               in, err := createInContext(inPaths[i]); if err != nil {
+                       fmt.Printf("Error processing %s: %s", inPaths[i], err.Error())
+                       os.Exit(1)
+               }
+               defer in.Cleanup()
+               err = in.Split(); if err != nil {
+                       fmt.Printf("%s\n", err.Error())
+                       os.Exit(1)
+               }
+               paths, err := in.GetPaths(); if err != nil {
+                       fmt.Printf("Error processing %s: %s", inPaths[i], err.Error())
+                       os.Exit(1)
+               }
+               for j := range paths {
+                       err = outDir.CopyFile(paths[j]); if err != nil {
+                               fmt.Printf("Error processing %s: %s", inPaths[i], err.Error())
+                               os.Exit(1)
+                       }
+               }
+               fmt.Printf("Split %s into %d file(s)\n", inPaths[i], len(paths))
+               totalFiles += int64(len(paths))
+       }
+       fmt.Printf("Processed %d total file(s)\n", totalFiles)
+}