package main import ( "os/exec" "bufio" "io" "path/filepath" "io/ioutil" "fmt" "math" "os" "sort" "path" "strings" "strconv" ) 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 } func (out *OutDir) GetNumDigitsNeeded() int { i := math.Log10(float64(out.nextIndex)) j := math.Floor(i) if j < i { return 1 + int(j) } else { return int(j) } } func (out *OutDir) RenameOutputFiles() error { numDigitsNeeded := out.GetNumDigitsNeeded() fs := "%0" + strconv.Itoa(numDigitsNeeded) + "d" + MP3_SUFFIX d, err := os.Open(out.outPath); if err != nil { return fmt.Errorf("Unable to open directory %s: %s", out.outPath, err.Error()) } defer d.Close() names, err := d.Readdirnames(0); if err != nil && err != io.EOF { return fmt.Errorf("Unable to readdir %s: %s", out.outPath, err.Error()) } for i := range names { index := -1 _, err = fmt.Sscanf(names[i], "%d", &index); if err != nil { return fmt.Errorf("Unable to parse file name %s", names[i]) } newName := fmt.Sprintf(fs, index) src := path.Join(out.outPath, names[i]) dst := path.Join(out.outPath, newName) err = os.Rename(src, dst) if err != nil { return fmt.Errorf("Failed to rename %s to %s: %s", src, dst, err.Error()) } } return nil } 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)) } err = outDir.RenameOutputFiles() if err != nil { fmt.Printf("Error renaming output files in %s: %s", outDir.outPath, err.Error()) os.Exit(1) } fmt.Printf("Processed %d total file(s)\n", totalFiles) }